UNPKG

@juzi/wechaty-puppet-whatsapp

Version:
283 lines 13.4 kB
import * as PUPPET from '@juzi/wechaty-puppet'; import { MessageMediaTypeList, log, } from '../../config.js'; import { MessageTypes as WhatsAppMessageType, MessageAck, } from '../../schema/whatsapp-interface.js'; import WhatsAppBase from '../whatsapp-base.js'; import { SpecialSystemType, } from '../../schema/whatsapp-type.js'; import { isContactId, isInviteLink, getInviteCode, sleep, } from '../../helper/miscellaneous.js'; import { RequestPool } from '../../request/request-pool.js'; import { v4 } from 'uuid'; const PRE = 'MessageEventHandler'; export default class MessageEventHandler extends WhatsAppBase { async onMessage(message) { log.info(PRE, `onMessage(${JSON.stringify(message)})`); if (!(await this.checkCacheManager())) { log.warn('message ignored because login process is not finished'); return; } const _data = message._data; // @ts-ignore if (message.type === 'multi_vcard' || (message.type === 'e2e_notification' && message.body === '' && !message.author && _data.subtype !== 'encrypt')) { // skip room join notification and multi_vcard message return; } const cacheManager = await this.manager.getCacheManager(); const messageId = message.id.id; const messageInCache = await cacheManager.getMessageRawPayload(messageId); if (messageInCache) { return; } if (_data.type === 'notification_template' && _data.subtype === 'contact_info_card') { message.type = WhatsAppMessageType.TEXT; message.body = '[客户通过广告或其他渠道发起对话]'; } if (_data.type === 'e2e_notification' && _data.subtype === 'encrypt') { message.type = SpecialSystemType; message.body = '消息和通话已进行端到端加密。只有此聊天中的成员可以查看、收听或分享。'; } await cacheManager.setMessageRawPayload(messageId, message); if (message._data?.caption && message._data?.type === 'image') { // see issue: https://github.com/wechaty/puppet-whatsapp/issues/390 // file message also have captions, but no text message should be generated const genTextMessageFromImageMessage = message; genTextMessageFromImageMessage.type = WhatsAppMessageType.TEXT; const textMsgId = `${genTextMessageFromImageMessage.id.id}_TEXT`; genTextMessageFromImageMessage.id.id = textMsgId; genTextMessageFromImageMessage._data = undefined; await this.onMessage(genTextMessageFromImageMessage); } const contactId = message.from; if (contactId && isContactId(contactId)) { const contact = await cacheManager.getContactOrRoomRawPayload(contactId); const notFriend = !contact?.isMyContact; if (notFriend) { const friendship = { id: v4(), contactId, hello: message.body, timestamp: message.timestamp, type: PUPPET.types.Friendship.Receive, ticket: '', }; await cacheManager.setFriendshipRawPayload(friendship.id, friendship); this.emit('friendship', { friendshipId: friendship.id }); } } const needEmitMessage = await this.convertInviteLinkMessageToEvent(message); if (needEmitMessage) { this.emit('message', { messageId }); } } /** * This event only for the message which sent by bot (web / phone) * @param {WhatsAppMessage} message message detail info * @returns */ async onMessageAck(message) { log.silly(PRE, `onMessageAck(${JSON.stringify(message)})`); if (!(await this.checkCacheManager())) { log.warn('message ignored because login process is not finished'); return; } /** * if message ack equal MessageAck.ACK_DEVICE, we could regard it as has already send success. * * FIXME: if the ack is not consecutive, and without MessageAck.ACK_DEVICE, then we could not receive this message. * * After add sync missed message schedule, if the ack of message has not reach MessageAck.ACK_DEVICE, * the schedule will emit these messages with wrong ack (ack = MessageAck.ACK_PENDING or MessageAck.ACK_SERVER), * and will make some mistakes (can not get the media of message). */ if (message.id.fromMe) { if (MessageMediaTypeList.includes(message.type)) { if (message.hasMedia && message.ack === MessageAck.ACK_SERVER) { await this.processMessageFromBot(message); } if (message.ack === MessageAck.ACK_DEVICE || message.ack === MessageAck.ACK_READ) { await this.processMessageFromBot(message); } } else { await this.processMessageFromBot(message); } } } /** * This event only for the message which sent by bot (web / phone) and to the bot self * @param {WhatsAppMessage} message message detail info * @returns */ async onMessageCreate(message) { log.silly(PRE, `onMessageCreate(${JSON.stringify(message)})`); if (!(await this.checkCacheManager())) { log.warn('message ignored because login process is not finished'); return; } if (message.id.fromMe) { const messageId = message.id.id; const cacheManager = await this.manager.getCacheManager(); await cacheManager.setMessageRawPayload(messageId, message); // void sleep // const requestPool = RequestPool.Instance // const now = Date.now() // while (!requestPool.hasRequest(messageId) && Date.now() - now < 400) { // await sleep(100) // } // requestPool.resolveRequest(messageId) await sleep(1000); // wait for sent message method return to avoid duplicate message // self sent message is not time sensitive, so we can wait for a while this.emit('message', { messageId }); } } async processMessageFromBot(message) { const messageId = message.id.id; const cacheManager = await this.manager.getCacheManager(); const messageInCache = await cacheManager.getMessageRawPayload(messageId); await cacheManager.setMessageRawPayload(messageId, message); // set message with different message ack /** * - Non-Media Message * emit only when no cache * * - Media Message * emit message when no cache or ack of message in cache equal 1 */ if (!messageInCache || (MessageMediaTypeList.includes(message.type) && messageInCache.ack === MessageAck.ACK_SERVER)) { if (!message.author) { // based on experience, not officially conformed // self message from other device contains author // while sent from this puppet it's undefined log.info(PRE, `seems to be self sent message, so skip. id: ${messageId}, base content: ${message.body}`); return; } const requestPool = RequestPool.Instance; const hasRequest = requestPool.resolveRequest(messageId); if (!hasRequest) { this.emit('message', { messageId }); } } if (messageInCache && message.id.fromMe && message.ack > messageInCache.ack && [MessageAck.ACK_READ, MessageAck.ACK_PLAYED].includes(message.ack)) { await cacheManager.setMessageRawPayload(messageId, message); this.emit('dirty', { payloadId: messageId, payloadType: PUPPET.types.Dirty.Message, }); } } async convertInviteLinkMessageToEvent(message) { const cacheManager = await this.manager.getCacheManager(); if (message.type === WhatsAppMessageType.GROUP_INVITE) { const inviteCode = message.inviteV4?.inviteCode; if (inviteCode) { const roomInvitationPayload = { roomInvitationId: inviteCode, }; await cacheManager.setRoomInvitationRawPayload(inviteCode, { inviteCode }); this.emit('room-invite', roomInvitationPayload); } else { log.warn(PRE, `convertInviteLinkMessageToEvent can not get invite code: ${JSON.stringify(message)}`); } return false; } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (message.type === WhatsAppMessageType.TEXT && message.links && message.links.length === 1 && isInviteLink(message.links[0].link)) { const inviteCode = getInviteCode(message.links[0].link); if (inviteCode) { const roomInvitationPayload = { roomInvitationId: inviteCode, }; await cacheManager.setRoomInvitationRawPayload(inviteCode, { inviteCode }); this.emit('room-invite', roomInvitationPayload); return false; } } return true; } async onIncomingCall(...args) { log.silly(PRE, `onIncomingCall(${JSON.stringify(args)})`); } async onMediaUploaded(message) { log.silly(PRE, `onMediaUploaded(${JSON.stringify(message)})`); await this.createOrUpdateImageMessage(message); if (!message.hasMedia) { log.warn(PRE, `onMediaUploaded failed, message id: ${message.id.id}, type: ${message.type}, detail info: ${JSON.stringify(message)}`); } } async createOrUpdateImageMessage(message) { if (message.type === WhatsAppMessageType.IMAGE) { const messageId = message.id.id; const cacheManager = await this.manager.getCacheManager(); const messageInCache = await cacheManager.getMessageRawPayload(messageId); if (messageInCache) { message.body = messageInCache.body || message.body; await cacheManager.setMessageRawPayload(messageId, message); return; } await cacheManager.setMessageRawPayload(messageId, message); } } /** * Someone delete message in all devices. Due to they have the same message id so we generate a fake id as flash-store key. * see: https://github.com/pedroslopez/whatsapp-web.js/issues/1178 * @param message revoke message * @param revokedMsg original message, sometimes it will be null */ async onMessageRevokeEveryone(message, revokedMsg) { log.silly(PRE, `onMessageRevokeEveryone(newMsg: ${JSON.stringify(message)}, originalMsg: ${JSON.stringify(revokedMsg)})`); if (!(await this.checkCacheManager())) { log.warn('message ignored because login process is not finished'); return; } const cacheManager = await this.manager.getCacheManager(); const messageId = message.id.id; if (revokedMsg) { const originalMessageId = revokedMsg.id.id; const recalledMessageId = this.generateFakeRecallMessageId(originalMessageId); message.body = recalledMessageId; await cacheManager.setMessageRawPayload(recalledMessageId, revokedMsg); } await cacheManager.setMessageRawPayload(messageId, message); this.emit('message', { messageId }); } /** * Only delete message in bot phone will trigger this event. But the message type is chat, not revoked any more. */ async onMessageRevokeMe(message) { log.silly(PRE, `onMessageRevokeMe(${JSON.stringify(message)})`); if (!(await this.checkCacheManager())) { log.warn('message ignored because login process is not finished'); } /* if (message.ack === MessageAck.ACK_PENDING) { // when the bot logout, it will receive onMessageRevokeMe event, but it's ack is MessageAck.ACK_PENDING, so let's ignore this event. return } const cacheManager = await this.manager.getCacheManager() const messageId = message.id.id message.type = WhatsAppMessageType.REVOKED message.body = messageId const recalledMessageId = this.generateFakeRecallMessageId(messageId) await cacheManager.setMessageRawPayload(recalledMessageId, message) this.emit('message', { messageId: recalledMessageId }) */ } generateFakeRecallMessageId(messageId) { return `${messageId}_revoked`; } async checkCacheManager() { let cacheManager; try { cacheManager = await this.manager.getCacheManager(); } catch (e) { } if (!cacheManager) { log.warn(PRE, 'message comes before login process finished'); return false; } return true; } } //# sourceMappingURL=message-event-handler.js.map