/** * Wechaty - https://github.com/chatie/wechaty * * @copyright 2016-2018 Huan LI * * 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 * as PUPPET from '@juzi/wechaty-puppet' import type { FileBoxInterface } from 'file-box' import { FileBox } from 'file-box' import { initServer } from './server/server.js' import { config, log, VERSION } from './config.js' import type { MiniContactPayload, MiniMessagePayload } from './help/struct.js' import { sendFileMessage, sendLinkMessage, sendPostMessage, sendTextMessage } from './help/message.js' import { CacheManager } from './cache/cacheManager.js' import mqtt from 'mqtt' import { v4 } from 'uuid' import dayjs from 'dayjs' import Hashids from 'hashids' export type PuppetMiniOptions = PUPPET.PuppetOptions & { chatbotId?: string // juzi 返回的 botId token?: string // 平台对接的 token port?: number, // koa 服务的端口号 mqName?: string // mqtt 的用户名 mqPassword?: string // mqtt 的密码 mqHost?: string // mqtt 链接地址 mqPort?: number // mqtt 链接端口 encodeKey: string // mqtt 链接端口 encodeIv: string // mqtt 链接端口 serviceUrl?: string // 获取用户信息的接口请求地址 serviceSecret?: string // jwt secret serviceSalt?: string // 加密salt bmpUrl?: string bmpAppKey?: string bmpAppSecret?: string mongoServer?: string feedPageKey?: string } const PRE = '[PuppetMini]' class PuppetMini extends PUPPET.Puppet { static port: number static token: string static chatbotId: string static instance: any static notifyUrlPrefix: string static mqttServer: any static mqttName: string static mqttPd: string static mqttPort: number static mqttHost: string static mqttStatus: null | string static encodeKey: string static encodeIv: string static serviceUrl: string static serviceSecret: string static serviceSalt: string static bmpUrl: string static bmpAppKey: string static bmpAppSecret: string static mongoServer: string static cacheManager: CacheManager static feedPageKey: string private _heartBeatTimer?: ReturnType static override readonly VERSION = VERSION constructor (options?: PuppetMiniOptions) { super(options) PuppetMini.instance = this PuppetMini.port = options?.port || (process.env['WECHATY_MINI_PORT'] && parseInt(process.env['WECHATY_MINI_PORT'])) || config.port PuppetMini.token = options?.token || process.env['WECHATY_TOKEN'] || '' PuppetMini.chatbotId = options?.chatbotId || process.env['WECHATY_MINI_CHATBOTID'] || '' PuppetMini.encodeKey = options?.encodeKey || process.env['WECHATY_MINI_ENCODE_KEY'] || '' PuppetMini.encodeIv = options?.encodeIv || process.env['WECHATY_MINI_ENCODE_IV'] || '' PuppetMini.mqttName = options?.mqName || process.env['WECHATY_MINI_MQNAME'] || '' PuppetMini.mqttPd = options?.mqPassword || process.env['WECHATY_MINI_MQPD'] || '' PuppetMini.mqttHost = options?.mqHost || process.env['WECHATY_MINI_MQHOST'] || config.mqUrl PuppetMini.mqttPort = options?.mqPort || (process.env['WECHATY_MINI_MQPORT'] && parseInt(process.env['WECHATY_MINI_MQPORT'])) || config.mqPort PuppetMini.serviceUrl = options?.serviceUrl || process.env['WECHATY_MINI_SERVICE_URL'] || config.serviceUrl PuppetMini.serviceSecret = options?.serviceSecret || process.env['WECHATY_MINI_SERVICE_SECRET'] || config.serviceSecret PuppetMini.serviceSalt = options?.serviceSalt || process.env['WECHATY_MINI_SERVICE_SALT'] || config.serviceSalt PuppetMini.bmpUrl = options?.bmpUrl || process.env['WECHATY_MINI_BMP_URL'] || config.bmpUrl PuppetMini.bmpAppKey = options?.bmpAppKey || process.env['WECHATY_MINI_BMP_APP_KEY'] || config.bmpAppKey PuppetMini.bmpAppSecret = options?.bmpAppSecret || process.env['WECHATY_MINI_BMP_APP_SECRET'] || config.bmpAppSecret PuppetMini.mongoServer = options?.mongoServer || process.env['WECHATY_MINI_MONGO_SERVER'] || config.mongoServer PuppetMini.feedPageKey = options?.feedPageKey || process.env['WECHATY_MINI_FEED_PAGE_KEY'] || '' PuppetMini.mqttStatus = null if (!PuppetMini.token || !PuppetMini.chatbotId || !PuppetMini.mqttName || !PuppetMini.mqttPd) { throw new Error('Set your Environment variables') } log.verbose('PuppetMini', 'constructor("%s")', JSON.stringify(options)) PuppetMini.cacheManager = new CacheManager() } override async onStart (): Promise { await this._startPuppetHeart(true) await initServer(PuppetMini.port, PuppetMini.token, PuppetMini.mongoServer) if (!PuppetMini.mqttServer) { PuppetMini.mqttServer = mqtt.connect(PuppetMini.mqttHost, { clean: true, clientId: PuppetMini.token, connectTimeout: 60000, password: PuppetMini.mqttPd, port: PuppetMini.mqttPort, username: PuppetMini.mqttName, }) PuppetMini.mqttServer.on('connect', () => { log.info(PRE, 'mqtt Connected') PuppetMini.mqttStatus = 'success' }) } await PuppetMini.cacheManager.init() /** * 初始化当前机器人的信息 */ const hashids = new Hashids(PuppetMini.serviceSalt) await PuppetMini.cacheManager.setContact(PuppetMini.chatbotId, { alias: '', avatar: config.avatarUrl, hashId: hashids.encode(PuppetMini.chatbotId.toString().split('')), id: PuppetMini.chatbotId, name: 'JuziBot', phone: PuppetMini.chatbotId, }) this.login(PuppetMini.chatbotId) setTimeout(() => { log.info(PRE, 'emit ready') this.emit('ready', { data: 'data ready', }) }, 5000) } // 开始监听心跳 private async _startPuppetHeart (firstTime: boolean = true) { if (firstTime && this._heartBeatTimer) { return } this.emit('heartbeat', { data: 'heartbeat@mini: live' }) // eslint-disable-next-line @typescript-eslint/no-misused-promises this._heartBeatTimer = setTimeout(async (): Promise => { await this._startPuppetHeart(false) return undefined }, 15 * 1000) // 15s } // 停止监听心跳 private _stopPuppetHeart () { if (!this._heartBeatTimer) { return } clearTimeout(this._heartBeatTimer) this._heartBeatTimer = undefined } override async onStop (): Promise { log.verbose(PRE, 'onStop()') if (this.isLoggedIn) { await this.logout() } PuppetMini.mqttServer && PuppetMini.mqttServer.close() PuppetMini.mqttServer = null PuppetMini.mqttStatus = null await PuppetMini.cacheManager.stop() this._stopPuppetHeart() return Promise.resolve(undefined) } override async logout (): Promise { if (!this.isLoggedIn) { log.verbose(PRE, 'logout() do nothing') return } this.emit('logout', { contactId: PuppetMini.chatbotId, data: 'logout by self' }) return Promise.resolve(undefined) } override ding (data?: string): void { log.silly(PRE, 'ding(%s)', data || '') setTimeout(() => this.emit('dong', { data: data || '' }), 1000) } /** * * Contact * */ override contactSelfName (_name: string): Promise { throw new Error('Method not implemented.') } override contactSelfQRCode (): Promise { throw new Error('Method not implemented.') } override contactSelfSignature (_signature: string): Promise { throw new Error('Method not implemented.') } override contactAlias(contactId: string): Promise override contactAlias(contactId: string, alias: string | null): Promise override async contactAlias (contactId: string, alias?: string | null): Promise { log.verbose(PRE, 'contactAlias(%s, %s)', contactId, alias) if (typeof alias === 'undefined') { return 'mock alias' } if (alias !== null) { await PuppetMini.cacheManager.setContactAlias(contactId, alias) } } override async contactPhone(contactId: string): Promise override async contactPhone(contactId: string, phoneList: string[]): Promise override async contactPhone (contactId: string, phoneList?: string[]): Promise { log.verbose(PRE, 'contactPhone(%s, %s)', contactId, phoneList) if (typeof phoneList === 'undefined') { return [] } } override async contactCorporationRemark (contactId: string, corporationRemark: string) { log.verbose(PRE, 'contactCorporationRemark(%s, %s)', contactId, corporationRemark) } override async contactDescription (contactId: string, description: string) { log.verbose(PRE, 'contactDescription(%s, %s)', contactId, description) } override async contactList (): Promise { log.verbose(PRE, 'contactList()') return await PuppetMini.cacheManager.getContactList(PuppetMini.chatbotId) } override async contactAvatar(contactId: string): Promise override async contactAvatar(contactId: string, file: FileBoxInterface): Promise override async contactAvatar (contactId: string, file?: FileBoxInterface): Promise { log.verbose(PRE, 'contactAvatar(%s)', contactId) const contact = await PuppetMini.cacheManager.getContact(contactId) if (file) { return } if (contact?.avatar) { return FileBox.fromUrl(contact.avatar) } return FileBox.fromUrl(config.avatarUrl) } override async contactRawPayloadParser (rawPayload: MiniContactPayload): Promise { return { alias: rawPayload.alias, avatar: rawPayload.avatar || '', friend: true, gender: PUPPET.types.ContactGender.Unknown, id: rawPayload.id, name: rawPayload.name || '', phone: rawPayload.phone ? [ rawPayload.phone ] : [], type: PUPPET.types.Contact.Individual, } } override async contactRawPayload (contactId: string): Promise { log.verbose(PRE, 'contactRawPayload(%s)', contactId) return PuppetMini.cacheManager.getContact(contactId) } /** * * Message * */ override async messageRawPayload (messageId: string): Promise { log.verbose(PRE, 'messageRawPayload(%s)', messageId) return PuppetMini.cacheManager.getMessage(messageId) } override async messageRawPayloadParser (rawPayload: MiniMessagePayload): Promise { log.verbose(PRE, 'messageRawPayloadParser(%s)', rawPayload.id) const res = { id: rawPayload.id, listenerId: rawPayload.listenerId, quoteId: rawPayload.quoteId, talkerId: rawPayload.talkerId, text: rawPayload.text, timestamp: rawPayload.timestamp, type: rawPayload.type, } return res } override async messageImage (messageId: string, imageType: PUPPET.types.Image): Promise { log.verbose(PRE, 'messageImage(%s, %s)', messageId, imageType) const file = await PuppetMini.cacheManager.getFile(messageId) if (!file) { throw new Error('File Not Found!') } return file } override async messageFile (messageId: string): Promise { log.verbose(PRE, 'messageFile(%s)', messageId) const file = await PuppetMini.cacheManager.getFile(messageId) if (!file) { throw new Error('File Not Found!') } return file } override async messageSendText (contactId: string, msg: string, options?: PUPPET.types.MessageSendTextOptions): Promise { log.verbose(PRE, 'messageSendText(%s, %s)', contactId, msg) const chatBotInfo = await PuppetMini.cacheManager.getContact(PuppetMini.chatbotId) const msgId = v4().replaceAll('-', '') // @ts-ignore const quoteId = options?.quoteId await PuppetMini.cacheManager.setMessage(msgId, { id: msgId, // 消息 id listenerId: contactId, // 设置的默认juzi 机器人 id quoteId, talkerAvatar: chatBotInfo?.avatar, // 聊天人头像 talkerId: PuppetMini.chatbotId, // 小程序界面上对话人的 id talkerName: chatBotInfo?.name, // 聊天人昵称 text: msg, timestamp: dayjs().unix(), type: PUPPET.types.Message.Text, }) sendTextMessage({ contactId, mqtt: PuppetMini.mqttServer, msg, msgId, quoteId, token: PuppetMini.token, }) return msgId } override async messageSendFile (contactId: string, file: FileBoxInterface): Promise { log.verbose(PRE, 'messageSendFile(%s, %s)', contactId, file) const msgId = v4().replaceAll('-', '') const chatBotInfo = await PuppetMini.cacheManager.getContact(PuppetMini.chatbotId) await PuppetMini.cacheManager.setMessage(msgId, { id: msgId, // 消息 id listenerId: contactId, // 设置的默认juzi 机器人 id talkerAvatar: chatBotInfo?.avatar, // 聊天人头像 talkerId: PuppetMini.chatbotId, // 小程序界面上对话人的 id talkerName: chatBotInfo?.name, // 聊天人昵称 text: file.name, timestamp: dayjs().unix(), type: PUPPET.types.Message.Attachment, }) await PuppetMini.cacheManager.setFile(msgId, file) await sendFileMessage({ contactId, file, mqtt: PuppetMini.mqttServer, msgId, token: PuppetMini.token, }) return msgId } override async messageSendUrl (contactId: string, linkPayload: PUPPET.payloads.UrlLink): Promise { log.verbose(PRE, 'messageSendUrl(%s, %s)', contactId, linkPayload.url) const msgId = v4().replaceAll('-', '') const chatBotInfo = await PuppetMini.cacheManager.getContact(PuppetMini.chatbotId) await PuppetMini.cacheManager.setMessage(msgId, { id: msgId, // 消息 id listenerId: contactId, // 设置的默认juzi 机器人 id talkerAvatar: chatBotInfo?.avatar, // 聊天人头像 talkerId: PuppetMini.chatbotId, // 小程序界面上对话人的 id talkerName: chatBotInfo?.name, // 聊天人昵称 text: linkPayload.title, timestamp: dayjs().unix(), type: PUPPET.types.Message.Url, }) let media if (linkPayload.thumbnailUrl) { media = FileBox.fromUrl(linkPayload.thumbnailUrl) as FileBoxInterface } else if (linkPayload.thumbnailFileBox) { media = linkPayload.thumbnailFileBox } await PuppetMini.cacheManager.setFile(msgId, media) await sendLinkMessage({ contactId, file: media, linkPayload, mqtt: PuppetMini.mqttServer, msgId, token: PuppetMini.token, }) return msgId } override async messageSendPost (contactId: string, postPayload: PUPPET.payloads.Post): Promise { log.verbose('PuppetMini', 'messageSendPost(%s, %s)', contactId, JSON.stringify(postPayload)) const msgId = v4().replaceAll('-', '') const chatBotInfo = await PuppetMini.cacheManager.getContact(PuppetMini.chatbotId) await PuppetMini.cacheManager.setMessage(msgId, { id: msgId, // 消息 id listenerId: contactId, quoteId: postPayload.parentId, // 设置的默认juzi 机器人 id talkerAvatar: chatBotInfo?.avatar, // 聊天人头像 talkerId: PuppetMini.chatbotId, // 小程序界面上对话人的 id talkerName: chatBotInfo?.name, // 聊天人昵称 text: postPayload.type?.toString() || '', timestamp: dayjs().unix(), type: PUPPET.types.Message.Post, }) log.verbose('PuppetMini', 'messageSendPost mqtt status(%s)', PuppetMini.mqttStatus) await sendPostMessage({ contactId, mqtt: PuppetMini.mqttServer, msgId, postPayload, quoteId: postPayload.parentId, token: PuppetMini.token, }) return msgId } override async postRawPayload (postId: string): Promise { log.verbose(PRE, 'postRawPayload(%s)', postId) return { postId } as any } override async postRawPayloadParser (rawPayload: any): Promise { log.verbose(PRE, 'postRawPayloadParser(%s)', rawPayload.id) return rawPayload } } export default PuppetMini