@juzi/wechaty
Version:
Wechaty is a RPA SDK for Chatbot Makers.
1,572 lines (1,360 loc) • 46.3 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 { 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,
}