UNPKG

@juzi/wechaty

Version:

Wechaty is a RPA SDK for Chatbot Makers.

1,572 lines (1,360 loc) 46.3 kB
/** * 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 { EventEmitter } from 'events' import * as PUPPET from '@juzi/wechaty-puppet' import type { FileBoxInterface, } from 'file-box' import type { Constructor } from 'clone-class' import { escapeRegExp } from '../pure-functions/escape-regexp.js' import { timestampToDate } from '../pure-functions/timestamp-to-date.js' import { log, AT_SEPARATOR_REGEX, } from '../config.js' import type { SayableSayer, Sayable, } from '../sayable/mod.js' import { messageToSayable, } from '../sayable/mod.js' import { wechatifyMixin, } from '../user-mixins/wechatify.js' import type { ContactInterface, ContactImpl, } from './contact.js' import type { RoomInterface, RoomImpl, } from './room.js' import type { UrlLinkInterface, } from './url-link.js' import type { MiniProgramInterface, } from './mini-program.js' import type { ImageInterface, } from './image.js' import type { PostInterface, } from './post.js' import type { LocationInterface, } from './location.js' import type { ChannelInterface, } from './channel.js' import type { ChannelCardInterface, } from './channel-card.js' import type { ConsultCardInterface, } from './consult-card.js' import type { PremiumOnlineAppointmentCardInterface, } from './premium-online-appointment-card.js' import { validationMixin } from '../user-mixins/validation.js' import type { ContactSelfImpl } from './contact-self.js' import { concurrencyExecuter } from 'rx-queue' import type { CallRecordInterface } from './call.js' import type { ChatHistoryInterface } from './chat-history.js' import type { WxxdProductInterface } from './wxxd-product.js' import type { WxxdOrderInterface } from './wxxd-order.js' const MixinBase = wechatifyMixin( EventEmitter, ) const ALLOW_PREVIEW_TYPES = [ PUPPET.types.Message.Image, PUPPET.types.Message.MiniProgram, PUPPET.types.Message.Video, PUPPET.types.Message.Url, PUPPET.types.Message.Channel, PUPPET.types.Message.ChannelCard, PUPPET.types.Message.Post, ] /** * All wechat messages will be encapsulated as a Message. * * [Examples/Ding-Dong-Bot]{@link https://github.com/wechaty/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/ding-dong-bot.ts} */ class MessageMixin extends MixinBase implements SayableSayer { /** * * Static Properties * */ /** * @ignore */ static readonly Type = PUPPET.types.Message /** * Find message in cache */ static async find ( query : string | PUPPET.filters.Message, ): Promise<undefined | MessageInterface> { log.verbose('Message', 'find(%s)', JSON.stringify(query)) if (typeof query === 'object' && query.id) { const message = this.load(query.id) try { await message.ready() } catch (e) { this.wechaty.emitError(e) return undefined } return message } if (typeof query === 'string') { query = { text: query } } const messageList = await this.findAll(query) if (messageList.length < 1) { return undefined } if (messageList.length > 1) { log.warn('Message', 'findAll() got more than one(%d) result', messageList.length) } return messageList[0]! } /** * Find messages in cache */ static async findAll ( query? : PUPPET.filters.Message, ): Promise<MessageInterface[]> { log.verbose('Message', 'findAll(%s)', JSON.stringify(query) || '') // Huan(202111): { id } query has been optimized in the PuppetAbstract class const invalidDict: { [id: string]: true } = {} try { const MessageIdList = await this.wechaty.puppet.messageSearch(query) const messageList = MessageIdList.map(id => this.load(id)) await Promise.all( messageList.map( message => message.ready() .catch(e => { log.warn('Room', 'findAll() message.ready() rejection: %s', e) invalidDict[message.id] = true }), ), ) return messageList.filter(message => !invalidDict[message.id]) } catch (e) { this.wechaty.emitError(e) log.warn('Message', 'findAll() rejected: %s', (e as Error).message) return [] // fail safe } } /** * Create a Mobile Terminated Message * @ignore * "mobile originated" or "mobile terminated" * https://www.tatango.com/resources/video-lessons/video-mo-mt-sms-messaging/ */ static load (id: string): MessageImplInterface { log.verbose('Message', 'static load(%s)', id) /** * Must NOT use `Message` at here * MUST use `this` at here * * because the class will be `cloneClass`-ed */ const msg = new this(id) return msg } static async getBroadcastTargets (): Promise<{ contacts: ContactInterface[]; rooms: RoomInterface[] }> { log.verbose('Message', 'static getBroadcastTargets()') const { contactIds = [], roomIds = [] } = await this.wechaty.puppet.getMessageBroadcastTarget() const CONCURRENCY = 17 let roomContinuousErrorCount = 0 let roomTotalErrorCount = 0 const roomTotalErrorThreshold = Math.round(roomIds.length / 5) const idToRoom = async (id: string) => { if (!this.wechaty.isLoggedIn) { throw new Error('wechaty not logged in') } const result = await this.wechaty.Room.find({ id }).catch(e => { this.wechaty.emitError(e) roomContinuousErrorCount++ roomTotalErrorCount++ if (roomContinuousErrorCount > 5) { throw new Error('5 continuous errors!') } if (roomTotalErrorCount > roomTotalErrorThreshold) { throw new Error(`${roomTotalErrorThreshold} total errors!`) } }) roomContinuousErrorCount = 0 return result } const roomIterator = concurrencyExecuter(CONCURRENCY)(idToRoom)(roomIds) const roomList: RoomInterface[] = [] for await (const room of roomIterator) { if (room) { roomList.push(room) } } let contactContinuousErrorCount = 0 let contactTotalErrorCount = 0 const contactTotalErrorThreshold = Math.round(roomIds.length / 5) const idToContact = async (id: string) => { if (!this.wechaty.isLoggedIn) { throw new Error('wechaty not logged in') } const result = await this.wechaty.Contact.find({ id }).catch(e => { this.wechaty.emitError(e) contactContinuousErrorCount++ contactTotalErrorCount++ if (contactContinuousErrorCount > 5) { throw new Error('5 continuous errors!') } if (contactTotalErrorCount > contactTotalErrorThreshold) { throw new Error(`${contactTotalErrorThreshold} total errors!`) } }) contactContinuousErrorCount = 0 return result } const contactIterator = concurrencyExecuter(CONCURRENCY)(idToContact)(contactIds) const contactList: ContactInterface[] = [] for await (const contact of contactIterator) { if (contact) { contactList.push(contact) } } return { contacts: contactList, rooms: roomList, } } static async createBroadcast (targets: (ContactInterface | RoomInterface)[], post: PostInterface): Promise<PostInterface | void> { log.verbose('Message', 'static createBroadcast()') const targetIds = targets.map(target => target.id) const type = post.payload.type || PUPPET.types.Post.Unspecified if (type !== PUPPET.types.Post.Broadcast) { throw new Error(`you cannot create broadcast with type ${PUPPET.types.Post[type]}`) } const postId = await this.wechaty.puppet.createMessageBroadcast(targetIds, post.payload) if (postId) { return this.wechaty.Post.find({ id: postId }) } } static async getBroadcastStatus (broadcast: PostInterface): Promise<{ status: PUPPET.types.BroadcastStatus, detail: { contact?: ContactInterface, room?: RoomInterface, status: PUPPET.types.BroadcastTargetStatus, }[] }> { log.verbose('Message', 'static getBroadcastStatus()') const postId = broadcast.id if (!postId) { throw new Error('cannot get status from a post with no Id') } const type = broadcast.payload.type if (type !== PUPPET.types.Post.Broadcast) { throw new Error(`cannot get status from a ${PUPPET.types.Post[type || 0]} post`) } const result = await this.wechaty.puppet.getMessageBroadcastStatus(postId) const broadcastStatus: { status: PUPPET.types.BroadcastStatus, detail: { contact?: ContactInterface, room?: RoomInterface, status: PUPPET.types.BroadcastTargetStatus, }[] } = { status: result.status, detail: [], } const CONCURRENCY = 17 const targetStatusToWechatyTargetStatus = async (detail: { contactId?: string, roomId?: string, status: PUPPET.types.BroadcastTargetStatus, }) => { let contact: ContactInterface | undefined let room: RoomInterface | undefined if (detail.contactId) { try { contact = await this.wechaty.Contact.find({ id: detail.contactId }) } catch (e) { this.wechaty.emitError(e) contact = undefined } } if (detail.roomId) { try { room = await this.wechaty.Room.find({ id: detail.roomId }) } catch (e) { this.wechaty.emitError(e) room = undefined } } return { contact, room, status: detail.status, } } const targetStatusIterator = concurrencyExecuter(CONCURRENCY)(targetStatusToWechatyTargetStatus)(result.detail) for await (const targetStatus of targetStatusIterator) { if (targetStatus.contact || targetStatus.room) { broadcastStatus.detail.push(targetStatus) } } return broadcastStatus } static async mergeForward (to: ContactInterface | RoomInterface, messageList: MessageInterface[]): Promise<void | MessageInterface> { log.verbose('Message', `mergeForward(${messageList})`) try { const msgId = await this.wechaty.puppet.messageForward( to.id, messageList.map(msg => msg.id), ) if (msgId) { const msg = await this.wechaty.Message.find({ id: msgId }) return msg } } catch (e) { log.error('Message', 'forward(%s) exception: %s', to, e) throw e } } /** * * Instance Properties * @hidden * */ payload?: PUPPET.payloads.Message /** * @hideconstructor */ constructor ( public readonly id: string, ) { super() log.verbose('Message', 'constructor(%s) for class %s', id || '', this.constructor.name, ) } /** * @ignore */ override toString () { if (!this.payload) { return this.constructor.name } let talker try { talker = this.talker() } catch (e) { talker = (e as Error).message } const msgStrList = [ 'Message', `#${PUPPET.types.Message[this.type()]}`, '[', '🗣', talker, this.room() ? '@👥' + this.room() : '', ']', ] if (this.type() === PUPPET.types.Message.Text || this.type() === PUPPET.types.Message.Unknown ) { msgStrList.push(`\t${this.text().substr(0, 70)}`) } else { log.silly('Message', 'toString() for message type: %s(%s)', PUPPET.types.Message[this.type()], this.type()) // if (!this.#payload) { // throw new Error('no payload') // } } return msgStrList.join('') } conversation (): ContactInterface | RoomInterface { if (this.room()) { return this.room()! } else { return this.talker() } } /** * Get the talker of a message. * @returns {ContactInterface} * @example * const bot = new Wechaty() * bot * .on('message', async m => { * const talker = msg.talker() * const text = msg.text() * const room = msg.room() * if (room) { * const topic = await room.topic() * console.log(`Room: ${topic} Contact: ${talker.name()} Text: ${text}`) * } else { * console.log(`Contact: ${talker.name()} Text: ${text}`) * } * }) * .start() */ talker (): ContactInterface { if (!this.payload) { throw new Error('no payload') } let talkerId = this.payload.talkerId if (!talkerId) { /** * `fromId` is deprecated, this code block will be removed in v2.0 */ if (this.payload.fromId) { talkerId = this.payload.fromId } else { throw new Error('no talkerId found for talker') } log.warn('Message', 'talker() payload.talkerId not exist! See: https://github.com/wechaty/puppet/issues/187') console.error('Puppet: %s@%s', this.wechaty.puppet.name(), this.wechaty.puppet.version()) console.error(new Error().stack) } let talker if (this.wechaty.isLoggedIn && talkerId === this.wechaty.puppet.currentUserId) { talker = (this.wechaty.ContactSelf as typeof ContactSelfImpl).load(talkerId) } else { talker = (this.wechaty.Contact as typeof ContactImpl).load(talkerId) } return talker } /** * @depreacated Use `message.talker()` to replace `message.from()` * https://github.com/wechaty/wechaty/issues/2094 */ from (): undefined | ContactInterface { log.warn('Message', 'from() is deprecated, use talker() instead. Call stack: %s', new Error().stack, ) try { return this.talker() } catch (e) { this.wechaty.emitError(e) return undefined } } /** * Get the destination of the message * Message.to() will return null if a message is in a room, use Message.room() to get the room. * @returns {(ContactInterface|null)} * @deprecated use `listener()` instead */ to (): undefined | ContactInterface { // Huan(202108): I want to deprecate this method name in the future, // and use `message.listener()` to replace it. return this.listener() } /** * Get the destination of the message * Message.listener() will return null if a message is in a room, * use Message.room() to get the room. * @returns {(undefined | ContactInterface)} */ listener (): undefined | ContactInterface { if (!this.payload) { throw new Error('no payload') } let listenerId = this.payload.listenerId if (!listenerId && this.payload.toId) { /** * `toId` is deprecated, this code block will be removed in v2.0 */ listenerId = this.payload.toId log.warn('Message', 'listener() payload.listenerId should be set! See: https://github.com/wechaty/puppet/issues/187') console.error('Puppet: %s@%s', this.wechaty.puppet.name(), this.wechaty.puppet.version()) console.error(new Error().stack) } if (!listenerId) { return undefined } let listener if (listenerId === this.wechaty.puppet.currentUserId) { listener = (this.wechaty.ContactSelf as typeof ContactSelfImpl).load(listenerId) } else { listener = (this.wechaty.Contact as typeof ContactImpl).load(listenerId) } return listener } /** * Get the room from the message. * If the message is not in a room, then will return `null` * * @returns {(RoomInterface | null)} * @example * const bot = new Wechaty() * bot * .on('message', async m => { * const contact = msg.from() * const text = msg.text() * const room = msg.room() * if (room) { * const topic = await room.topic() * console.log(`Room: ${topic} Contact: ${contact.name()} Text: ${text}`) * } else { * console.log(`Contact: ${contact.name()} Text: ${text}`) * } * }) * .start() */ room (): undefined | RoomInterface { if (!this.payload) { throw new Error('no payload') } const roomId = this.payload.roomId if (!roomId) { return undefined } const room = (this.wechaty.Room as typeof RoomImpl).load(roomId) return room } /** * Get the text content of the message * * @returns {string} * @example * const bot = new Wechaty() * bot * .on('message', async m => { * const contact = msg.from() * const text = msg.text() * const room = msg.room() * if (room) { * const topic = await room.topic() * console.log(`Room: ${topic} Contact: ${contact.name()} Text: ${text}`) * } else { * console.log(`Contact: ${contact.name()} Text: ${text}`) * } * }) * .start() */ text (): string { if (!this.payload) { throw new Error('no payload') } const oldText = this.payload.text || '' const newText = (this.payload.textContent || []).map(item => item.text).join('') if (newText && oldText !== newText) { log.warn('Message', `got different text, old: ${oldText}, new: ${newText}`) } // still use old text before we deprecate old text field return oldText } /** * Get the recalled message * * @example * const bot = new Wechaty() * bot * .on('message', async m => { * if (m.type() === PUPPET.types.Message.Recalled) { * const recalledMessage = await m.toRecalled() * console.log(`Message: ${recalledMessage} has been recalled.`) * } * }) * .start() */ async toRecalled (): Promise<undefined | MessageInterface> { if (this.type() !== PUPPET.types.Message.Recalled) { throw new Error('Can not call toRecalled() on message which is not recalled type.') } const originalMessageId = this.text() if (!originalMessageId) { throw new Error('Can not find recalled message') } try { const message = await this.wechaty.Message.find({ id: originalMessageId }) if (message) { return message } } catch (e) { this.wechaty.emitError(e) log.verbose(`Can not retrieve the recalled message with id ${originalMessageId}.`) } return undefined } /** * Reply a Text or Media File message to the sender. * > Tips: * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) * * @see {@link https://github.com/wechaty/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/ding-dong-bot.ts|Examples/ding-dong-bot} * @param {(string | ContactInterface | FileBox | UrlLinkInterface | MiniProgramInterface | LocationInterface)} textOrContactOrFile * send text, Contact, or file to bot. </br> * You can use {@link https://www.npmjs.com/package/file-box|FileBox} to send file * @param {(ContactInterface|ContactInterface[])} [mention] * If this is a room message, when you set mention param, you can `@` Contact in the room. * @returns {Promise<void | MessageInterface>} * * @example * import { FileBox } from 'wechaty' * const bot = new Wechaty() * bot * .on('message', async m => { * * // 1. send Image * * if (/^ding$/i.test(m.text())) { * const fileBox = FileBox.fromUrl('https://wechaty.github.io/wechaty/images/bot-qr-code.png') * await msg.say(fileBox) * const message = await msg.say(fileBox) // only supported by puppet-padplus * } * * // 2. send Text * * if (/^dong$/i.test(m.text())) { * await msg.say('ding') * const message = await msg.say('ding') // only supported by puppet-padplus * } * * // 3. send Contact * * if (/^lijiarui$/i.test(m.text())) { * const contactCard = await bot.Contact.find({name: 'lijiarui'}) * if (!contactCard) { * console.log('not found') * return * } * await msg.say(contactCard) * const message = await msg.say(contactCard) // only supported by puppet-padplus * } * * // 4. send Link * * if (/^link$/i.test(m.text())) { * 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 msg.say(linkPayload) * const message = await msg.say(linkPayload) // only supported by puppet-padplus * } * * // 5. send MiniProgram * * if (/^miniProgram$/i.test(m.text())) { * const miniProgramPayload = 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 msg.say(miniProgramPayload) * const message = await msg.say(miniProgramPayload) // only supported by puppet-padplus * } * * // 6. send Location * if (/^location$/i.test(m.text())) { * const location = new Location ({ * accuracy : 15, * address : '北京市北京市海淀区45 Chengfu Rd', * latitude : 39.995120999999997, * longitude : 116.334154, * name : '东升乡人民政府(海淀区成府路45号)', * }) * await contact.say(location) * const msg = await msg.say(location) * } * }) * .start() */ async say ( sayable: Sayable, ): Promise<void | MessageInterface> { log.verbose('Message', 'say(%s)', sayable) const talker = this.talker() const room = this.room() if (room) { return room.say(sayable) } else { return talker.say(sayable) } } /** * Reply a message while quoting the original one * @param text * @param mentionIdList */ async reply ( text: string, mentionList?: ContactInterface[], ): Promise<void | MessageInterface> { log.verbose('Message', 'say(%s)', text) const talker = this.talker() const room = this.room() if (room) { return room.say(text, { mentionList, quoteMessage: this, }) } else { return talker.say(text, { quoteMessage: this, }) } } /** * Recall a message. * > Tips: * @returns {Promise<boolean>} * * @example * const bot = new Wechaty() * bot * .on('message', async m => { * const recallMessage = await msg.say('123') * if (recallMessage) { * const isSuccess = await recallMessage.recall() * } * }) */ async recall (): Promise<boolean> { log.verbose('Message', 'recall()') const isSuccess = await this.wechaty.puppet.messageRecall(this.id) return isSuccess } /** * Get the type from the message. * > Tips: PUPPET.types.Message is Enum here. </br> * - PUPPET.types.Message.Unknown </br> * - PUPPET.types.Message.Attachment </br> * - PUPPET.types.Message.Audio </br> * - PUPPET.types.Message.Contact </br> * - PUPPET.types.Message.Emoticon </br> * - PUPPET.types.Message.Image </br> * - PUPPET.types.Message.Text </br> * - PUPPET.types.Message.Video </br> * - PUPPET.types.Message.Url </br> * @returns {PUPPET.types.Message} * * @example * const bot = new Wechaty() * if (message.type() === bot.Message.Type.Text) { * console.log('This is a text message') * } */ type (): PUPPET.types.Message { if (!this.payload) { throw new Error('no payload') } return this.payload.type || PUPPET.types.Message.Unknown } /** * Check if a message is sent by self. * * @returns {boolean} - Return `true` for send from self, `false` for send from others. * @example * if (message.self()) { * console.log('this message is sent by myself!') * } */ self (): boolean { try { const talker = this.talker() return talker.id === this.wechaty.puppet.currentUserId } catch (e) { this.wechaty.emitError(e) log.error('Message', 'self() rejection: %s', (e as Error).message) return false } } /** * * Get message mentioned contactList. * * Message event table as follows * * | | Web | Mac PC Client | iOS Mobile | android Mobile | * | :--- | :--: | :----: | :---: | :---: | * | [You were mentioned] tip ([有人@我]的提示) | ✘ | √ | √ | √ | * | Identify magic code (8197) by copy & paste in mobile | ✘ | √ | √ | ✘ | * | Identify magic code (8197) by programming | ✘ | ✘ | ✘ | ✘ | * | Identify two contacts with the same roomAlias by [You were mentioned] tip | ✘ | ✘ | √ | √ | * * @returns {Promise<ContactInterface[]>} - Return message mentioned contactList * * @example * const contactList = await message.mentionList() * console.log(contactList) */ async mentionList (): Promise<ContactInterface[]> { log.verbose('Message', 'mentionList()') const room = this.room() if (this.type() !== PUPPET.types.Message.Text || !room) { return [] } /** * 1. Use mention list if mention list is available */ if (this.payload && 'mentionIdList' in this.payload && Array.isArray(this.payload.mentionIdList) ) { const contactList = await (this.wechaty.Contact as any as typeof ContactImpl).batchLoadContacts(this.payload.mentionIdList) // remove `undefined` types because we use a `filter(Boolean)` return contactList } /** * 2. Otherwise, process the message and get the mention list */ /** * define magic code `8197` to identify @xxx * const AT_SEPARATOR = String.fromCharCode(8197) */ const atList = this.text().split(AT_SEPARATOR_REGEX) // console.log('atList: ', atList) if (atList.length === 0) return [] // Using `filter(e => e.indexOf('@') > -1)` to filter the string without `@` const rawMentionList = atList .filter(str => str.includes('@')) .map(str => multipleAt(str)) // convert 'hello@a@b@c' to [ 'c', 'b@c', 'a@b@c' ] function multipleAt (str: string) { str = str.replace(/^.*?@/, '@') let name = '' const nameList: string[] = [] str.split('@') .filter(mentionName => !!mentionName) .reverse() .forEach(mentionName => { // console.log('mentionName: ', mentionName) name = mentionName + '@' + name nameList.push(name.slice(0, -1)) // get rid of the `@` at beginning }) return nameList } let mentionNameList: string[] = [] // Flatten Array // see http://stackoverflow.com/a/10865042/1123955 mentionNameList = mentionNameList.concat.apply([], rawMentionList) // filter blank string mentionNameList = mentionNameList.filter(s => !!s) log.verbose('Message', 'mentionList() text = "%s", mentionNameList = "%s"', this.text(), JSON.stringify(mentionNameList), ) const contactListNested = await Promise.all( mentionNameList.map( name => room.memberAll(name), ), ) let contactList: ContactInterface[] = [] contactList = contactList.concat.apply([], contactListNested) if (contactList.length === 0) { log.silly('Message', `message.mentionList() can not found member using room.member() from mentionList, mention string: ${JSON.stringify(mentionNameList)}`) } return contactList } isMentionAll (): boolean { log.verbose('Message', 'isMentionAll()') const room = this.room() if (this.type() !== PUPPET.types.Message.Text || !room) { return false } if (this.payload && 'mentionIdList' in this.payload && Array.isArray(this.payload.mentionIdList) ) { return this.payload.mentionIdList.some(id => id === this.payload?.talkerId) } else { return false } } /** * @deprecated mention() DEPRECATED. use mentionList() instead. */ async mention (): Promise<ContactInterface[]> { log.warn('Message', 'mention() DEPRECATED. use mentionList() instead. Call stack: %s', new Error().stack, ) return this.mentionList() } async mentionText (): Promise<string> { const text = this.text() const room = this.room() const mentionList = await this.mentionList() if (!room || mentionList.length === 0) { return text } if (this.payload?.textContent?.length) { const text = this.payload.textContent.map(item => { const type = item.type switch (type) { case PUPPET.types.TextContentType.Regular: return item.text case PUPPET.types.TextContentType.At: return '' default: log.warn(`got unknown type ${type} in text content`) return '' } }).join('') return text } else { const toAliasName = async (member: ContactInterface) => { const alias = await room.alias(member) const name = member.name() return alias || name } const mentionNameList = await Promise.all(mentionList.map(toAliasName)) const textWithoutMention = mentionNameList.reduce((prev, cur) => { const escapedCur = escapeRegExp(cur) const regex = new RegExp(`@${escapedCur}(\u2005|\u0020|$)`) return prev.replace(regex, '') }, text) return textWithoutMention.trim() } } /** * Check if a message is mention self. * * @returns {boolean} - Return `true` for mention me. * @example * if (message.mentionSelf()) { * console.log('this message were mentioned me! [You were mentioned] tip ([有人@我]的提示)') * } */ mentionSelf (): boolean { if (this.payload?.roomId && 'mentionIdList' in this.payload && Array.isArray(this.payload.mentionIdList) ) { const currentUserId = this.wechaty.puppet.currentUserId // const mentionList = await this.mentionList() const mentionIdList = this.payload.mentionIdList return mentionIdList.some(id => id === currentUserId) } else { return false } } /** * @ignore */ isReady (): boolean { return !!this.payload } /** * @ignore */ async ready (forceSync = false): Promise<void> { log.verbose('Message', 'ready()') if (this.isReady() && !forceSync) { return } this.payload = await this.wechaty.puppet.messagePayload(this.id) let talkerId = this.payload.talkerId if (!talkerId) { /** * `fromId` is deprecated: this code block will be removed in v2.0 */ if (this.payload.fromId) { talkerId = this.payload.fromId } else { throw new Error('no talkerId found for talker') } log.warn('Message', 'ready() payload.talkerId not exist! See: https://github.com/wechaty/puppet/issues/187') console.error('Puppet: %s@%s', this.wechaty.puppet.name(), this.wechaty.puppet.version()) console.error(new Error().stack) } const roomId = this.payload.roomId let listenerId = this.payload.listenerId if (!listenerId && this.payload.toId) { /** * `fromId` is deprecated: this code block will be removed in v2.0 */ listenerId = this.payload.toId log.warn('Message', 'ready() payload.listenerId should be set! See: https://github.com/wechaty/puppet/issues/187') console.error('Puppet: %s@%s', this.wechaty.puppet.name(), this.wechaty.puppet.version()) console.error(new Error().stack) } if (roomId) { await this.wechaty.Room.find({ id: roomId }) } if (talkerId) { await this.wechaty.Contact.find({ id: talkerId }) } if (listenerId) { await this.wechaty.Contact.find({ id: listenerId }) } } // case WebMsgType.APP: // if (!this.rawObj) { // throw new Error('no rawObj') // } // switch (this.typeApp()) { // case WebAppMsgType.ATTACH: // if (!this.rawObj.MMAppMsgDownloadUrl) { // throw new Error('no MMAppMsgDownloadUrl') // } // // had set in Message // // url = this.rawObj.MMAppMsgDownloadUrl // break // case WebAppMsgType.URL: // case WebAppMsgType.READER_TYPE: // if (!this.rawObj.Url) { // throw new Error('no Url') // } // // had set in Message // // url = this.rawObj.Url // break // default: // const e = new Error('ready() unsupported typeApp(): ' + this.typeApp()) // log.warn('PuppeteerMessage', e.message) // throw e // } // break // case WebMsgType.TEXT: // if (this.typeSub() === WebMsgType.LOCATION) { // url = await puppet.bridge.getMsgPublicLinkImg(this.id) // } // break /** * Forward the received message. * * @param {(Sayable | Sayable[])} to Room or Contact * The recipient of the message, the room, or the contact * @returns {Promise<void>} * @example * const bot = new Wechaty() * bot * .on('message', async m => { * const room = await bot.Room.find({topic: 'wechaty'}) * if (room) { * await m.forward(room) * console.log('forward this message to wechaty room!') * } * }) * .start() */ async forward (to: RoomInterface | ContactInterface): Promise<void | MessageInterface> { log.verbose('Message', 'forward(%s)', to) // let roomId // let contactId try { const msgId = await this.wechaty.puppet.messageForward( to.id, this.id, ) if (msgId) { const msg = await this.wechaty.Message.find({ id: msgId }) return msg } } catch (e) { log.error('Message', 'forward(%s) exception: %s', to, e) throw e } } /** * Message sent date */ date (): Date { if (!this.payload) { throw new Error('no payload') } const timestamp = this.payload.timestamp return timestampToDate(timestamp) } /** * Returns the message age in seconds. <br> * * For example, the message is sent at time `8:43:01`, * and when we received it in Wechaty, the time is `8:43:15`, * then the age() will return `8:43:15 - 8:43:01 = 14 (seconds)` * * @returns {number} message age in seconds. */ age (): number { const ageMilliseconds = Date.now() - this.date().getTime() const ageSeconds = Math.floor(ageMilliseconds / 1000) return ageSeconds } /** * Extract the Media File from the Message, and put it into the FileBox. * > Tips: * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) * * @returns {Promise<FileBoxInterface>} * * @example <caption>Save media file from a message</caption> * const fileBox = await message.toFileBox() * const fileName = fileBox.name * fileBox.toFile(fileName) */ async toFileBox (): Promise<FileBoxInterface> { log.verbose('Message', 'toFileBox()') if (this.type() === PUPPET.types.Message.Text) { throw new Error('text message no file') } const fileBox = await this.wechaty.puppet.messageFile(this.id) return fileBox } /** * Extract the Image File from the Message, so that we can use different image sizes. * > Tips: * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) * * @returns {ImageInterface} * * @example <caption>Save image file from a message</caption> * const image = message.toImage() * const fileBox = await image.artwork() * const fileName = fileBox.name * fileBox.toFile(fileName) */ toImage (): ImageInterface { log.verbose('Message', 'toImage() for message id: %s', this.id) if (this.type() !== PUPPET.types.Message.Image) { throw new Error(`not a image type message. type: ${this.type()}`) } return this.wechaty.Image.create(this.id) } async toPreview (): Promise<FileBoxInterface | undefined> { log.verbose('Message', 'toPreview() for message id: %s', this.id) if (!ALLOW_PREVIEW_TYPES.some(e => e === this.type())) { throw new Error(`cannot get preview for this type. type: ${this.type}`) } return this.wechaty.puppet.messagePreview(this.id) } /** * Get Share Card of the Message * Extract the Contact Card from the Message, and encapsulate it into Contact class * > Tips: * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) * @returns {Promise<ContactInterface>} */ async toContact (): Promise<ContactInterface> { log.verbose('Message', 'toContact()') if (this.type() !== PUPPET.types.Message.Contact) { throw new Error('message not a ShareCard') } const contactId = await this.wechaty.puppet.messageContact(this.id) if (!contactId) { throw new Error(`can not get Contact id by message: ${contactId}`) } const contact = await this.wechaty.Contact.find({ id: contactId }) if (!contact) { throw new Error(`can not get Contact payload by from id: ${contactId}`) } return contact } async toUrlLink (): Promise<UrlLinkInterface> { log.verbose('Message', 'toUrlLink()') if (!this.payload) { throw new Error('no payload') } if (this.type() !== PUPPET.types.Message.Url) { throw new Error('message not a Url Link') } const urlPayload = await this.wechaty.puppet.messageUrl(this.id) return new this.wechaty.UrlLink(urlPayload) } async toMiniProgram (): Promise<MiniProgramInterface> { log.verbose('Message', 'toMiniProgram()') if (!this.payload) { throw new Error('no payload') } if (this.type() !== PUPPET.types.Message.MiniProgram) { throw new Error('message not a MiniProgram') } const miniProgramPayload = await this.wechaty.puppet.messageMiniProgram(this.id) return new this.wechaty.MiniProgram(miniProgramPayload) } async toLocation (): Promise<LocationInterface> { log.verbose('Message', 'toLocation()') if (!this.payload) { throw new Error('no payload') } if (this.type() !== PUPPET.types.Message.Location) { throw new Error('message not a Location') } const locationPayload = await this.wechaty.puppet.messageLocation(this.id) return new this.wechaty.Location(locationPayload) } public async toPost (): Promise<PostInterface> { log.verbose('Message', 'toPost()') if (!this.payload) { throw new Error('no payload') } if (this.type() !== PUPPET.types.Message.Post) { throw new Error('message type not a Post') } const post = this.wechaty.Post.load(this.id) await post.ready() return post } public async toChannel (): Promise<ChannelInterface> { log.verbose('Message', 'toChannel()') if (!this.payload) { throw new Error('no payload') } if (this.type() !== PUPPET.types.Message.Channel) { throw new Error('message not a Channel') } const channelPayload = await this.wechaty.puppet.messageChannel(this.id) return new this.wechaty.Channel(channelPayload) } public async toChannelCard (): Promise<ChannelCardInterface> { log.verbose('Message', 'toChannelCard()') if (!this.payload) { throw new Error('no payload') } if (this.type() !== PUPPET.types.Message.ChannelCard) { throw new Error('message not a ChannelCard') } const channelCardPayload = await this.wechaty.puppet.messageChannelCard(this.id) return new this.wechaty.ChannelCard(channelCardPayload) } public async toConsultCard (): Promise<ConsultCardInterface> { log.verbose('Message', 'toConsultCard()') if (!this.payload) { throw new Error('no payload') } if (this.type() !== PUPPET.types.Message.ConsultCard) { throw new Error('message not a ConsultCard') } const consultCardPayload = await this.wechaty.puppet.messageConsultCard(this.id) return new this.wechaty.ConsultCard(consultCardPayload) } public async toPremiumOnlineAppointmentCard (): Promise<PremiumOnlineAppointmentCardInterface> { log.verbose('Message', 'toPremiumOnlineAppointmentCard()') if (!this.payload) { throw new Error('no payload') } if (this.type() !== PUPPET.types.Message.PremiumOnlineAppointmentCard) { throw new Error('message not a PremiumOnlineAppointmentCard') } const premiumOnlineAppointmentCardPayload = await this.wechaty.puppet.messagePremiumOnlineAppointmentCard(this.id) return new this.wechaty.PremiumOnlineAppointmentCard(premiumOnlineAppointmentCardPayload) } public async toCallRecord (): Promise<CallRecordInterface> { log.verbose('Message', 'toCallRecord()') if (!this.payload) { throw new Error('no payload') } if (this.type() !== PUPPET.types.Message.CallRecord) { throw new Error('message not a CallRecord') } const callRecordPayload = await this.wechaty.puppet.messageCallRecord(this.id) return new this.wechaty.CallRecord(callRecordPayload) } public async toChatHistory (): Promise<ChatHistoryInterface> { log.verbose('Message', 'toChatHistory()') if (!this.payload) { throw new Error('no payload') } if (this.type() !== PUPPET.types.Message.ChatHistory) { throw new Error('message not a ChatHistory') } const chatHistoryPayload = await this.wechaty.puppet.messageChatHistory(this.id) return new this.wechaty.ChatHistory(chatHistoryPayload) } public async toWxxdProduct (): Promise<WxxdProductInterface> { log.verbose('Message', 'toWxxdProduct()') if (!this.payload) { throw new Error('no payload') } if (this.type() !== PUPPET.types.Message.WxxdProduct) { throw new Error('message not a WxxdProduct') } const wxxdProductPayload = await this.wechaty.puppet.messageWxxdProduct(this.id) const product = new this.wechaty.WxxdProduct(wxxdProductPayload.productId) await product.ready() return product } public async toWxxdOrder (): Promise<WxxdOrderInterface> { log.verbose('Message', 'toWxxdOrder()') if (!this.payload) { throw new Error('no payload') } if (this.type() !== PUPPET.types.Message.WxxdOrder) { throw new Error('message not a WxxdOrder') } const wxxdOrderPayload = await this.wechaty.puppet.messageWxxdOrder(this.id) const order = new this.wechaty.WxxdOrder(wxxdOrderPayload.orderId) await order.ready() return order } async toSayable (): Promise<undefined | Sayable> { log.verbose('Message', 'toSayable()') return messageToSayable(this) } async getQuotedMessage (): Promise<undefined | MessageInterface> { log.verbose('Message', 'getQuotedMessage()') if (!this.payload) { throw new Error('no payload') } if (!this.payload.quoteId) { return } return this.wechaty.Message.find({ id: this.payload.quoteId }) } additionalInfo (): undefined | any { let additionalInfoObj = {} if (this.payload?.additionalInfo) { try { additionalInfoObj = JSON.parse(this.payload.additionalInfo) } catch (e) { log.warn('Message', 'additionalInfo() parse failed, additionalInfo: %s', this.payload.additionalInfo) } } return additionalInfoObj } sendError (): string | undefined { return this.payload?.sendError } readList (): string[] | undefined { return this.payload?.readList } } class MessageImplBase extends validationMixin(MessageMixin)<MessageImplInterface>() {} interface MessageImplInterface extends MessageImplBase {} type MessageProtectedProperty = | 'ready' type MessageInterface = Omit<MessageImplInterface, MessageProtectedProperty> class MessageImpl extends validationMixin(MessageImplBase)<MessageInterface>() {} type MessageConstructor = Constructor< MessageInterface, Omit<typeof MessageImpl, 'load'> > export type { MessageInterface, MessageProtectedProperty, MessageConstructor, } export { MessageImpl, }