@juzi/wechaty-puppet-whatsapp
Version:
Wechaty Puppet for WhatsApp
283 lines • 13.4 kB
JavaScript
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