|
import makeWASocket, { |
|
Browsers, |
|
DisconnectReason, |
|
getAggregateVotesInPollMessage, |
|
getKeyAuthor, |
|
isJidGroup, |
|
jidNormalizedUser, |
|
makeCacheableSignalKeyStore, |
|
makeInMemoryStore, |
|
PresenceData, |
|
proto, |
|
useMultiFileAuthState, |
|
WAMessageContent, |
|
WAMessageKey, |
|
} from '@adiwajshing/baileys'; |
|
import { UnprocessableEntityException } from '@nestjs/common'; |
|
import * as Buffer from 'buffer'; |
|
import { request } from 'express'; |
|
import * as fs from 'fs/promises'; |
|
import { Agent } from 'https'; |
|
import * as lodash from 'lodash'; |
|
import { PairingCodeResponse } from 'src/structures/auth.dto'; |
|
|
|
import { flipObject, splitAt } from '../../../helpers'; |
|
import { |
|
ChatRequest, |
|
CheckNumberStatusQuery, |
|
EditMessageRequest, |
|
MessageContactVcardRequest, |
|
MessageDestination, |
|
MessageFileRequest, |
|
MessageImageRequest, |
|
MessageLinkPreviewRequest, |
|
MessageLocationRequest, |
|
MessagePollRequest, |
|
MessageReactionRequest, |
|
MessageReplyRequest, |
|
MessageStarRequest, |
|
MessageTextRequest, |
|
MessageVoiceRequest, |
|
SendSeenRequest, |
|
WANumberExistResult, |
|
} from '../../../structures/chatting.dto'; |
|
import { ContactQuery, ContactRequest } from '../../../structures/contacts.dto'; |
|
import { |
|
ACK_UNKNOWN, |
|
SECOND, |
|
WAHAEngine, |
|
WAHAEvents, |
|
WAHAPresenceStatus, |
|
WAHASessionStatus, |
|
WAMessageAck, |
|
} from '../../../structures/enums.dto'; |
|
import { |
|
CreateGroupRequest, |
|
ParticipantsRequest, |
|
} from '../../../structures/groups.dto'; |
|
import { |
|
WAHAChatPresences, |
|
WAHAPresenceData, |
|
} from '../../../structures/presence.dto'; |
|
import { |
|
WAMessage, |
|
WAMessageReaction, |
|
} from '../../../structures/responses.dto'; |
|
import { MeInfo } from '../../../structures/sessions.dto'; |
|
import { BROADCAST_ID, TextStatus } from '../../../structures/status.dto'; |
|
import { |
|
PollVote, |
|
PollVotePayload, |
|
WAMessageAckBody, |
|
} from '../../../structures/webhooks.dto'; |
|
import { IEngineMediaProcessor } from '../../abc/media.abc'; |
|
import { |
|
ensureSuffix, |
|
WAHAInternalEvent, |
|
WhatsappSession, |
|
} from '../../abc/session.abc'; |
|
import { |
|
AvailableInPlusVersion, |
|
NotImplementedByEngineError, |
|
} from '../../exceptions'; |
|
import { toVcard } from '../../helpers'; |
|
import { createAgentProxy } from '../../helpers.proxy'; |
|
import { QR } from '../../QR'; |
|
import { NowebAuthFactoryCore } from './NowebAuthFactoryCore'; |
|
|
|
|
|
const QRCode = require('qrcode'); |
|
|
|
const logger = require('pino')(); |
|
|
|
export const BaileysEvents = { |
|
CONNECTION_UPDATE: 'connection.update', |
|
CREDS_UPDATE: 'creds.update', |
|
MESSAGES_UPDATE: 'messages.update', |
|
MESSAGES_UPSERT: 'messages.upsert', |
|
MESSAGE_RECEIPT_UPDATE: 'message-receipt.update', |
|
GROUPS_UPSERT: 'groups.upsert', |
|
PRESENCE_UPDATE: 'presence.update', |
|
}; |
|
|
|
const PresenceStatuses = { |
|
unavailable: WAHAPresenceStatus.OFFLINE, |
|
available: WAHAPresenceStatus.ONLINE, |
|
composing: WAHAPresenceStatus.TYPING, |
|
recording: WAHAPresenceStatus.RECORDING, |
|
paused: WAHAPresenceStatus.PAUSED, |
|
}; |
|
const ToEnginePresenceStatus = flipObject(PresenceStatuses); |
|
|
|
export class WhatsappSessionNoWebCore extends WhatsappSession { |
|
engine = WAHAEngine.NOWEB; |
|
authFactory = new NowebAuthFactoryCore(); |
|
private restartTimeoutId: null | ReturnType<typeof setTimeout> = null; |
|
|
|
get listenConnectionEventsFromTheStart() { |
|
return true; |
|
} |
|
|
|
sock: any; |
|
store: any; |
|
private qr: QR; |
|
|
|
public constructor(config) { |
|
super(config); |
|
this.qr = new QR(); |
|
} |
|
|
|
start() { |
|
this.status = WAHASessionStatus.STARTING; |
|
this.buildClient(); |
|
} |
|
|
|
getSocketConfig(agent, state) { |
|
return { |
|
agent: agent, |
|
fetchAgent: agent, |
|
auth: { |
|
creds: state.creds, |
|
|
|
keys: makeCacheableSignalKeyStore(state.keys, logger), |
|
}, |
|
printQRInTerminal: true, |
|
browser: Browsers.ubuntu('Chrome'), |
|
logger: logger, |
|
mobile: false, |
|
defaultQueryTimeoutMs: undefined, |
|
getMessage: (key) => this.getMessage(key), |
|
}; |
|
} |
|
|
|
async makeSocket() { |
|
const { state, saveCreds } = await this.authFactory.buildAuth( |
|
this.sessionStore, |
|
this.name, |
|
); |
|
const agent = this.makeAgent(); |
|
const socketConfig = this.getSocketConfig(agent, state); |
|
const sock: any = makeWASocket(socketConfig); |
|
sock.ev.on(BaileysEvents.CREDS_UPDATE, saveCreds); |
|
return sock; |
|
} |
|
|
|
protected makeAgent(): Agent { |
|
if (!this.proxyConfig) { |
|
return undefined; |
|
} |
|
return createAgentProxy(this.proxyConfig); |
|
} |
|
|
|
connectStore() { |
|
this.log.debug(`Connecting store...`); |
|
if (!this.store) { |
|
this.log.debug(`Making a new auth store...`); |
|
this.store = makeInMemoryStore({}); |
|
} |
|
this.log.debug(`Binding store to socket...`); |
|
this.store.bind(this.sock.ev); |
|
} |
|
|
|
resubscribeToKnownPresences() { |
|
for (const jid in this.store.presences) { |
|
this.sock.presenceSubscribe(jid); |
|
} |
|
} |
|
|
|
async buildClient() { |
|
this.sock = await this.makeSocket(); |
|
this.connectStore(); |
|
if (this.isDebugEnabled()) { |
|
this.listenEngineEventsInDebugMode(); |
|
} |
|
if (this.listenConnectionEventsFromTheStart) { |
|
this.listenConnectionEvents(); |
|
this.events.emit(WAHAInternalEvent.ENGINE_START); |
|
} |
|
} |
|
|
|
protected async getMessage( |
|
key: WAMessageKey, |
|
): Promise<WAMessageContent | undefined> { |
|
if (!this.store) { |
|
return proto.Message.fromObject({}); |
|
} |
|
const msg = await this.store.loadMessage(key.remoteJid, key.id); |
|
return msg?.message || undefined; |
|
} |
|
|
|
protected listenEngineEventsInDebugMode() { |
|
this.sock.ev.process((events) => { |
|
this.log.debug(`NOWEB events: ${JSON.stringify(events)}`); |
|
}); |
|
} |
|
|
|
private restartClient() { |
|
if (this.restartTimeoutId) { |
|
this.log.log( |
|
'Request to restart is already in progress, ignoring this request', |
|
); |
|
return; |
|
} |
|
this.log.log('Setting up client restart in 2 seconds...'); |
|
this.restartTimeoutId = setTimeout(() => { |
|
this.restartTimeoutId = undefined; |
|
this.buildClient(); |
|
}, 2 * SECOND); |
|
} |
|
|
|
protected listenConnectionEvents() { |
|
this.log.debug(`Start listening ${BaileysEvents.CONNECTION_UPDATE}...`); |
|
this.sock.ev.on(BaileysEvents.CONNECTION_UPDATE, async (update) => { |
|
const { connection, lastDisconnect, qr, isNewLogin } = update; |
|
if (isNewLogin) { |
|
this.restartClient(); |
|
} else if (connection === 'open') { |
|
this.qr.save(''); |
|
this.status = WAHASessionStatus.WORKING; |
|
this.resubscribeToKnownPresences(); |
|
return; |
|
} else if (connection === 'close') { |
|
const shouldReconnect = |
|
lastDisconnect.error?.output?.statusCode !== |
|
DisconnectReason.loggedOut; |
|
this.log.error( |
|
'connection closed due to ', |
|
lastDisconnect.error, |
|
', reconnecting ', |
|
shouldReconnect, |
|
); |
|
this.qr.save(''); |
|
|
|
if (shouldReconnect) { |
|
this.restartClient(); |
|
} else { |
|
this.status = WAHASessionStatus.FAILED; |
|
} |
|
} |
|
|
|
|
|
if (qr) { |
|
this.status = WAHASessionStatus.SCAN_QR_CODE; |
|
QRCode.toDataURL(qr).then((url) => { |
|
this.qr.save(url, qr); |
|
}); |
|
} |
|
}); |
|
} |
|
|
|
async stop() { |
|
this.sock?.ev?.removeAllListeners(); |
|
this.sock?.ws?.removeAllListeners(); |
|
this.sock?.ws?.close(); |
|
this.status = WAHASessionStatus.STOPPED; |
|
return; |
|
} |
|
|
|
async getSessionMeInfo(): Promise<MeInfo | null> { |
|
const me = this.sock.authState?.creds?.me; |
|
if (!me) { |
|
return null; |
|
} |
|
const meId = jidNormalizedUser(me.id); |
|
const meInfo: MeInfo = { |
|
id: toCusFormat(meId), |
|
pushName: me.name, |
|
}; |
|
return meInfo; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public getQR(): QR { |
|
return this.qr; |
|
} |
|
|
|
public async requestCode( |
|
phoneNumber: string, |
|
method: string, |
|
params?: any, |
|
): Promise<PairingCodeResponse> { |
|
if (method) { |
|
const err = `NOWEB engine doesn't support any 'method', remove it and try again`; |
|
throw new UnprocessableEntityException(err); |
|
} |
|
|
|
if (this.status == WAHASessionStatus.STARTING) { |
|
this.log.debug('Waiting for connection update...'); |
|
await this.sock.waitForConnectionUpdate((update) => !!update.qr); |
|
} |
|
|
|
if (this.status != WAHASessionStatus.SCAN_QR_CODE) { |
|
const err = `Can request code only in SCAN_QR_CODE status. The current status is ${this.status}`; |
|
throw new UnprocessableEntityException(err); |
|
} |
|
|
|
this.log.log(`Requesting pairing code for '${phoneNumber}'...`); |
|
const code: string = await this.sock.requestPairingCode(phoneNumber); |
|
|
|
const parts = splitAt(code, 4); |
|
const codeRepr = parts.join('-'); |
|
this.log.log(`Your code: ${codeRepr}`); |
|
return { code: codeRepr }; |
|
} |
|
|
|
async getScreenshot(): Promise<Buffer> { |
|
if (this.status === WAHASessionStatus.STARTING) { |
|
throw new UnprocessableEntityException( |
|
`The session is starting, please try again after few seconds`, |
|
); |
|
} else if (this.status === WAHASessionStatus.SCAN_QR_CODE) { |
|
return Promise.resolve(this.qr.get()); |
|
} else if (this.status === WAHASessionStatus.WORKING) { |
|
throw new UnprocessableEntityException( |
|
`Can not get screenshot for non chrome based engine.`, |
|
); |
|
} else { |
|
throw new UnprocessableEntityException(`Unknown status - ${this.status}`); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
async checkNumberStatus( |
|
request: CheckNumberStatusQuery, |
|
): Promise<WANumberExistResult> { |
|
const phone = request.phone.split('@')[0]; |
|
const [result] = await this.sock.onWhatsApp(phone); |
|
if (!result || !result.exists) { |
|
return { numberExists: false }; |
|
} |
|
return { |
|
numberExists: true, |
|
chatId: toCusFormat(result.jid), |
|
}; |
|
} |
|
|
|
sendText(request: MessageTextRequest) { |
|
const chatId = this.ensureSuffix(request.chatId); |
|
const message = { |
|
text: request.text, |
|
mentions: request.mentions?.map(toJID), |
|
}; |
|
return this.sock.sendMessage(chatId, message); |
|
} |
|
|
|
public deleteMessage(chatId: string, messageId: string) { |
|
const jid = toJID(this.ensureSuffix(chatId)); |
|
const key = parseMessageId(messageId); |
|
return this.sock.sendMessage(jid, { delete: key }); |
|
} |
|
|
|
public editMessage( |
|
chatId: string, |
|
messageId: string, |
|
request: EditMessageRequest, |
|
) { |
|
const jid = toJID(this.ensureSuffix(chatId)); |
|
const key = parseMessageId(messageId); |
|
const message = { |
|
text: request.text, |
|
mentions: request.mentions?.map(toJID), |
|
edit: key, |
|
}; |
|
return this.sock.sendMessage(jid, message); |
|
} |
|
|
|
async sendContactVCard(request: MessageContactVcardRequest) { |
|
const chatId = this.ensureSuffix(request.chatId); |
|
const contacts = request.contacts.map((el) => ({ vcard: toVcard(el) })); |
|
await this.sock.sendMessage(chatId, { |
|
contacts: { |
|
contacts: contacts, |
|
}, |
|
}); |
|
} |
|
|
|
async sendPoll(request: MessagePollRequest) { |
|
const requestPoll = request.poll; |
|
const poll = { |
|
name: requestPoll.name, |
|
values: requestPoll.options, |
|
selectableCount: requestPoll.multipleAnswers |
|
? requestPoll.options.length |
|
: 1, |
|
}; |
|
const message = { poll: poll }; |
|
const remoteJid = toJID(request.chatId); |
|
const result = await this.sock.sendMessage(remoteJid, message); |
|
return this.toWAMessage(result); |
|
} |
|
|
|
async reply(request: MessageReplyRequest) { |
|
const { id } = parseMessageId(request.reply_to); |
|
const quotedMessage = await this.store.loadMessage( |
|
toJID(request.chatId), |
|
id, |
|
); |
|
const message = { |
|
text: request.text, |
|
mentions: request.mentions?.map(toJID), |
|
}; |
|
return await this.sock.sendMessage(request.chatId, message, { |
|
quoted: quotedMessage, |
|
}); |
|
} |
|
|
|
sendImage(request: MessageImageRequest) { |
|
throw new AvailableInPlusVersion(); |
|
} |
|
|
|
sendFile(request: MessageFileRequest) { |
|
throw new AvailableInPlusVersion(); |
|
} |
|
|
|
sendVoice(request: MessageVoiceRequest) { |
|
throw new AvailableInPlusVersion(); |
|
} |
|
|
|
sendLocation(request: MessageLocationRequest) { |
|
return this.sock.sendMessage(request.chatId, { |
|
location: { |
|
degreesLatitude: request.latitude, |
|
degreesLongitude: request.longitude, |
|
}, |
|
}); |
|
} |
|
|
|
sendLinkPreview(request: MessageLinkPreviewRequest) { |
|
const text = `${request.title}\n${request.url}`; |
|
const chatId = this.ensureSuffix(request.chatId); |
|
return this.sock.sendMessage(chatId, { text: text }); |
|
} |
|
|
|
async sendSeen(request: SendSeenRequest) { |
|
const key = parseMessageId(request.messageId); |
|
const data = { |
|
remoteJid: key.remoteJid, |
|
id: key.id, |
|
participant: request.participant, |
|
}; |
|
return this.sock.readMessages([data]); |
|
} |
|
|
|
async startTyping(request: ChatRequest) { |
|
return this.sock.sendPresenceUpdate('composing', request.chatId); |
|
} |
|
|
|
async stopTyping(request: ChatRequest) { |
|
return this.sock.sendPresenceUpdate('paused', request.chatId); |
|
} |
|
|
|
async setReaction(request: MessageReactionRequest) { |
|
const key = parseMessageId(request.messageId); |
|
const reactionMessage = { |
|
react: { |
|
text: request.reaction, |
|
key: key, |
|
}, |
|
}; |
|
return this.sock.sendMessage(key.remoteJid, reactionMessage); |
|
} |
|
|
|
async setStar(request: MessageStarRequest) { |
|
const key = parseMessageId(request.messageId); |
|
await this.sock.chatModify( |
|
{ |
|
star: { |
|
messages: [{ id: key.id, fromMe: key.fromMe }], |
|
star: request.star, |
|
}, |
|
}, |
|
toJID(request.chatId), |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
getContact(query: ContactQuery) { |
|
throw new NotImplementedByEngineError(); |
|
} |
|
|
|
getContacts() { |
|
throw new NotImplementedByEngineError(); |
|
} |
|
|
|
public async getContactAbout(query: ContactQuery) { |
|
throw new NotImplementedByEngineError(); |
|
} |
|
|
|
public async getContactProfilePicture(query: ContactQuery) { |
|
throw new NotImplementedByEngineError(); |
|
} |
|
|
|
public async blockContact(request: ContactRequest) { |
|
throw new NotImplementedByEngineError(); |
|
} |
|
|
|
public async unblockContact(request: ContactRequest) { |
|
throw new NotImplementedByEngineError(); |
|
} |
|
|
|
|
|
|
|
|
|
public createGroup(request: CreateGroupRequest) { |
|
const participants = request.participants.map(getId); |
|
return this.sock.groupCreate(request.name, participants); |
|
} |
|
|
|
public async getGroups() { |
|
return await this.sock.groupFetchAllParticipating(); |
|
} |
|
|
|
public async getGroup(id) { |
|
const groups = await this.sock.groupFetchAllParticipating(); |
|
return groups[id]; |
|
} |
|
|
|
public async deleteGroup(id) { |
|
throw new NotImplementedByEngineError(); |
|
} |
|
|
|
public async leaveGroup(id) { |
|
return this.sock.groupLeave(id); |
|
} |
|
|
|
public async setDescription(id, description) { |
|
return this.sock.groupUpdateDescription(id, description); |
|
} |
|
|
|
public async setSubject(id, subject) { |
|
return this.sock.groupUpdateSubject(id, subject); |
|
} |
|
|
|
public async getInviteCode(id): Promise<string> { |
|
return this.sock.groupInviteCode(id); |
|
} |
|
|
|
public async revokeInviteCode(id): Promise<string> { |
|
await this.sock.groupRevokeInvite(id); |
|
return this.sock.groupInviteCode(id); |
|
} |
|
|
|
public async getParticipants(id) { |
|
const groups = await this.sock.groupFetchAllParticipating(); |
|
return groups[id].participants; |
|
} |
|
|
|
public async addParticipants(id, request: ParticipantsRequest) { |
|
const participants = request.participants.map(getId); |
|
return this.sock.groupParticipantsUpdate(id, participants, 'add'); |
|
} |
|
|
|
public async removeParticipants(id, request: ParticipantsRequest) { |
|
const participants = request.participants.map(getId); |
|
return this.sock.groupParticipantsUpdate(id, participants, 'remove'); |
|
} |
|
|
|
public async promoteParticipantsToAdmin(id, request: ParticipantsRequest) { |
|
const participants = request.participants.map(getId); |
|
return this.sock.groupParticipantsUpdate(id, participants, 'promote'); |
|
} |
|
|
|
public async demoteParticipantsToUser(id, request: ParticipantsRequest) { |
|
const participants = request.participants.map(getId); |
|
return this.sock.groupParticipantsUpdate(id, participants, 'demote'); |
|
} |
|
|
|
public async setPresence(presence: WAHAPresenceStatus, chatId?: string) { |
|
const enginePresence = ToEnginePresenceStatus[presence]; |
|
if (!enginePresence) { |
|
throw new NotImplementedByEngineError( |
|
`NOWEB engine doesn't support '${presence}' presence.`, |
|
); |
|
} |
|
await this.sock.sendPresenceUpdate(enginePresence, chatId); |
|
} |
|
|
|
public async getPresences(): Promise<WAHAChatPresences[]> { |
|
const result: WAHAChatPresences[] = []; |
|
for (const remoteJid in this.store.presences) { |
|
const storedPresences = this.store.presences[remoteJid]; |
|
result.push(this.toWahaPresences(remoteJid, storedPresences)); |
|
} |
|
return result; |
|
} |
|
|
|
public async getPresence(chatId: string): Promise<WAHAChatPresences> { |
|
await this.subscribePresence(chatId); |
|
const remoteJid = toJID(chatId); |
|
const storedPresences = this.store.presences[remoteJid]; |
|
return this.toWahaPresences(remoteJid, storedPresences || []); |
|
} |
|
|
|
public async subscribePresence(chatId: string): Promise<void> { |
|
const remoteJid = toJID(chatId); |
|
if (this.store.presences[remoteJid]) { |
|
return; |
|
} |
|
|
|
await this.sock.presenceSubscribe(remoteJid); |
|
return; |
|
} |
|
|
|
|
|
|
|
|
|
public sendTextStatus(status: TextStatus) { |
|
const message = { text: status.text }; |
|
const options = { |
|
backgroundColor: status.backgroundColor, |
|
font: status.font, |
|
statusJidList: status.contacts.map(toJID), |
|
}; |
|
return this.sock.sendMessage(BROADCAST_ID, message, options); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
subscribeEngineEvent(event, handler): boolean { |
|
switch (event) { |
|
case WAHAEvents.MESSAGE: |
|
this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) => { |
|
this.handleIncomingMessages(messages, handler, false); |
|
}); |
|
return true; |
|
case WAHAEvents.MESSAGE_REACTION: |
|
this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) => { |
|
const reactions = this.processMessageReaction(messages); |
|
reactions.map(handler); |
|
}); |
|
return true; |
|
case WAHAEvents.MESSAGE_ANY: |
|
this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) => |
|
this.handleIncomingMessages(messages, handler, true), |
|
); |
|
return true; |
|
case WAHAEvents.MESSAGE_ACK: |
|
this.sock.ev.on(BaileysEvents.MESSAGES_UPDATE, (events) => { |
|
events |
|
.filter(isMine) |
|
.filter(isAckUpdateMessageEvent) |
|
.map(this.convertMessageUpdateToMessageAck) |
|
.forEach(handler); |
|
}); |
|
|
|
this.sock.ev.on(BaileysEvents.MESSAGE_RECEIPT_UPDATE, (events) => { |
|
events |
|
.filter(isMine) |
|
.map(this.convertMessageReceiptUpdateToMessageAck) |
|
.forEach(handler); |
|
}); |
|
return true; |
|
case WAHAEvents.STATE_CHANGE: |
|
this.sock.ev.on(BaileysEvents.CONNECTION_UPDATE, handler); |
|
return true; |
|
case WAHAEvents.GROUP_JOIN: |
|
this.sock.ev.on(BaileysEvents.GROUPS_UPSERT, handler); |
|
return true; |
|
case WAHAEvents.PRESENCE_UPDATE: |
|
this.sock.ev.on(BaileysEvents.PRESENCE_UPDATE, (data) => |
|
handler(this.toWahaPresences(data.id, data.presences)), |
|
); |
|
return true; |
|
case WAHAEvents.POLL_VOTE: |
|
this.sock.ev.on(BaileysEvents.MESSAGES_UPDATE, (events) => { |
|
events.forEach((event) => |
|
this.handleMessagesUpdatePollVote(event, handler), |
|
); |
|
}); |
|
return true; |
|
case WAHAEvents.POLL_VOTE_FAILED: |
|
this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) => { |
|
messages.forEach((message) => |
|
this.handleMessageUpsertPollVoteFailed(message, handler), |
|
); |
|
}); |
|
return true; |
|
default: |
|
return false; |
|
} |
|
} |
|
|
|
private handleIncomingMessages(messages, handler, includeFromMe) { |
|
for (const message of messages) { |
|
|
|
if (!message) return; |
|
if (!message.message) return; |
|
|
|
if (message.message.reactionMessage) return; |
|
|
|
if (message.message.pollUpdateMessage) return; |
|
|
|
if (!includeFromMe && message.key.fromMe) { |
|
continue; |
|
} |
|
this.processIncomingMessage(message).then(handler); |
|
} |
|
} |
|
|
|
private processMessageReaction(messages: any[]): WAMessageReaction[] { |
|
const reactions = []; |
|
for (const message of messages) { |
|
if (!message) return []; |
|
if (!message.message) return []; |
|
if (!message.message.reactionMessage) return []; |
|
|
|
const id = buildMessageId(message.key); |
|
const fromToParticipant = getFromToParticipant(message); |
|
const reactionMessage = message.message.reactionMessage; |
|
const messageId = buildMessageId(reactionMessage.key); |
|
const reaction: WAMessageReaction = { |
|
id: id, |
|
timestamp: message.messageTimestamp, |
|
from: toCusFormat(fromToParticipant.from), |
|
fromMe: message.key.fromMe, |
|
to: toCusFormat(fromToParticipant.to), |
|
participant: toCusFormat(fromToParticipant.participant), |
|
reaction: { |
|
text: reactionMessage.text, |
|
messageId: messageId, |
|
}, |
|
}; |
|
reactions.push(reaction); |
|
} |
|
return reactions; |
|
} |
|
|
|
private processIncomingMessage(message) { |
|
return this.downloadMedia(message) |
|
.then(this.toWAMessage) |
|
.catch((error) => { |
|
this.log.error('Failed to process incoming message'); |
|
this.log.error(error); |
|
console.trace(error); |
|
}); |
|
} |
|
|
|
protected toWAMessage(message): Promise<WAMessage> { |
|
const fromToParticipant = getFromToParticipant(message); |
|
const id = buildMessageId(message.key); |
|
let body = message.message.conversation; |
|
if (!body) { |
|
|
|
|
|
body = message.message.extendedTextMessage?.text; |
|
} |
|
return Promise.resolve({ |
|
id: id, |
|
timestamp: message.messageTimestamp, |
|
from: toCusFormat(fromToParticipant.from), |
|
fromMe: message.key.fromMe, |
|
body: body, |
|
to: toCusFormat(fromToParticipant.to), |
|
participant: toCusFormat(fromToParticipant.participant), |
|
|
|
hasMedia: Boolean(message.media), |
|
media: message.media, |
|
mediaUrl: message.media?.url, |
|
|
|
ack: message.ack, |
|
|
|
ackName: WAMessageAck[message.ack] || ACK_UNKNOWN, |
|
location: message.location, |
|
vCards: message.vCards, |
|
_data: message, |
|
}); |
|
} |
|
|
|
protected convertMessageUpdateToMessageAck(event): WAMessageAckBody { |
|
const message = event; |
|
const fromToParticipant = getFromToParticipant(message); |
|
const id = buildMessageId(message.key); |
|
const ack = message.update.status - 1; |
|
const body: WAMessageAckBody = { |
|
id: id, |
|
from: toCusFormat(fromToParticipant.from), |
|
to: toCusFormat(fromToParticipant.to), |
|
participant: toCusFormat(fromToParticipant.participant), |
|
fromMe: message.key.fromMe, |
|
ack: ack, |
|
ackName: WAMessageAck[ack] || ACK_UNKNOWN, |
|
}; |
|
return body; |
|
} |
|
|
|
protected convertMessageReceiptUpdateToMessageAck(event): WAMessageAckBody { |
|
const fromToParticipant = getFromToParticipant(event); |
|
const id = buildMessageId(event.key); |
|
|
|
const receipt = event.receipt; |
|
let ack; |
|
if (receipt.receiptTimestamp) { |
|
ack = WAMessageAck.SERVER; |
|
} else if (receipt.playedTimestamp) { |
|
ack = WAMessageAck.PLAYED; |
|
} else if (receipt.readTimestamp) { |
|
ack = WAMessageAck.READ; |
|
} |
|
const body: WAMessageAckBody = { |
|
id: id, |
|
from: toCusFormat(fromToParticipant.from), |
|
to: toCusFormat(fromToParticipant.to), |
|
participant: toCusFormat(fromToParticipant.participant), |
|
fromMe: event.key.fromMe, |
|
ack: ack, |
|
ackName: WAMessageAck[ack] || ACK_UNKNOWN, |
|
}; |
|
return body; |
|
} |
|
|
|
protected async handleMessagesUpdatePollVote(event, handler) { |
|
const { key, update } = event; |
|
const pollUpdates = update?.pollUpdates; |
|
if (!pollUpdates) { |
|
return; |
|
} |
|
|
|
const pollCreationMessageKey = key; |
|
const pollCreationMessage = await this.getMessage(key); |
|
|
|
for (const pollUpdate of pollUpdates) { |
|
const votes = getAggregateVotesInPollMessage({ |
|
message: pollCreationMessage, |
|
pollUpdates: [pollUpdate], |
|
}); |
|
|
|
|
|
const selectedOptions = []; |
|
for (const voteAggregation of votes) { |
|
for (const voter of voteAggregation.voters) { |
|
if (voter === getKeyAuthor(pollUpdate.pollUpdateMessageKey)) { |
|
selectedOptions.push(voteAggregation.name); |
|
} |
|
} |
|
} |
|
|
|
|
|
const voteDestination = getDestination(pollUpdate.pollUpdateMessageKey); |
|
const pollVote: PollVote = { |
|
...voteDestination, |
|
selectedOptions: selectedOptions, |
|
timestamp: pollUpdate.senderTimestampMs, |
|
}; |
|
const payload: PollVotePayload = { |
|
vote: pollVote, |
|
poll: getDestination(pollCreationMessageKey), |
|
}; |
|
handler(payload); |
|
} |
|
} |
|
|
|
protected async handleMessageUpsertPollVoteFailed(message, handler) { |
|
const pollUpdateMessage = message.message?.pollUpdateMessage; |
|
if (!pollUpdateMessage) { |
|
return; |
|
} |
|
const pollCreationMessageKey = pollUpdateMessage.pollCreationMessageKey; |
|
const pollCreationMessage = await this.getMessage(pollCreationMessageKey); |
|
if (pollCreationMessage) { |
|
|
|
return; |
|
} |
|
|
|
|
|
const pollUpdateMessageKey = message.key; |
|
const voteDestination = getDestination(pollUpdateMessageKey); |
|
const pollVote: PollVote = { |
|
...voteDestination, |
|
selectedOptions: [], |
|
|
|
|
|
|
|
|
|
timestamp: message.messageTimestamp, |
|
}; |
|
const payload: PollVotePayload = { |
|
vote: pollVote, |
|
poll: getDestination(pollCreationMessageKey), |
|
}; |
|
handler(payload); |
|
} |
|
|
|
private toWahaPresences( |
|
remoteJid, |
|
storedPresences: PresenceData[], |
|
): WAHAChatPresences { |
|
const presences: WAHAPresenceData[] = []; |
|
for (const participant in storedPresences) { |
|
const data: PresenceData = storedPresences[participant]; |
|
const lastKnownPresence = lodash.get( |
|
PresenceStatuses, |
|
data.lastKnownPresence, |
|
data.lastKnownPresence, |
|
); |
|
const presence: WAHAPresenceData = { |
|
participant: toCusFormat(participant), |
|
|
|
lastKnownPresence: lastKnownPresence, |
|
lastSeen: data.lastSeen || null, |
|
}; |
|
presences.push(presence); |
|
} |
|
const chatId = toCusFormat(remoteJid); |
|
return { id: chatId, presences: presences }; |
|
} |
|
|
|
protected downloadMedia(message) { |
|
const processor = new EngineMediaProcessor(this); |
|
return this.mediaManager.processMedia(processor, message); |
|
} |
|
} |
|
|
|
export class EngineMediaProcessor implements IEngineMediaProcessor<any> { |
|
constructor(public session: WhatsappSessionNoWebCore) {} |
|
|
|
hasMedia(message: any): boolean { |
|
const messageType = Object.keys(message.message)[0]; |
|
const hasMedia = |
|
messageType === 'imageMessage' || |
|
messageType == 'audioMessage' || |
|
messageType == 'documentMessage' || |
|
messageType == 'videoMessage'; |
|
return hasMedia; |
|
} |
|
|
|
getMessageId(message: any): string { |
|
return ''; |
|
} |
|
|
|
getMimetype(message: any): string { |
|
return ''; |
|
} |
|
|
|
getMediaBuffer(message: any): Promise<Buffer | null> { |
|
return Promise.resolve(undefined); |
|
} |
|
|
|
getFilename(message: any): string | null { |
|
return message.message?.documentMessage?.fileName || null; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
function toCusFormat(remoteJid) { |
|
if (isJidGroup(remoteJid)) { |
|
return remoteJid; |
|
} |
|
if (!remoteJid) { |
|
return; |
|
} |
|
if (remoteJid == 'me') { |
|
return remoteJid; |
|
} |
|
const number = remoteJid.split('@')[0]; |
|
return ensureSuffix(number); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function toJID(chatId) { |
|
if (isJidGroup(chatId)) { |
|
return chatId; |
|
} |
|
const number = chatId.split('@')[0]; |
|
return number + '@s.whatsapp.net'; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function buildMessageId({ id, remoteJid, fromMe }) { |
|
const chatId = toCusFormat(remoteJid); |
|
return `${fromMe || false}_${chatId}_${id}`; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function parseMessageId(messageId) { |
|
const parts = messageId.split('_'); |
|
if (parts.length != 3) { |
|
throw new Error( |
|
'Message id be in format false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA', |
|
); |
|
} |
|
const fromMe = parts[0] == 'true'; |
|
const chatId = parts[1]; |
|
const remoteJid = toJID(chatId); |
|
const id = parts[2]; |
|
return { fromMe: fromMe, id: id, remoteJid: remoteJid }; |
|
} |
|
|
|
function getId(object) { |
|
return object.id; |
|
} |
|
|
|
function isMine(message) { |
|
return message?.key?.fromMe; |
|
} |
|
|
|
function isNotMine(message) { |
|
return !message?.key?.fromMe; |
|
} |
|
|
|
function isAckUpdateMessageEvent(event) { |
|
return event?.update.status != null; |
|
} |
|
|
|
function getFromToParticipant(message) { |
|
const isGroupMessage = Boolean(message.key.participant); |
|
let participant: string; |
|
let to: string; |
|
if (isGroupMessage) { |
|
participant = message.key.participant; |
|
to = message.key.remoteJid; |
|
} |
|
const from = message.key.remoteJid; |
|
return { |
|
from: from, |
|
to: to, |
|
participant: participant, |
|
}; |
|
} |
|
|
|
function getTo(key, meId = undefined) { |
|
|
|
const isGroupMessage = Boolean(key.participant); |
|
if (isGroupMessage) { |
|
return key.remoteJid; |
|
} |
|
if (key.fromMe) { |
|
return key.remoteJid; |
|
} |
|
return meId || 'me'; |
|
} |
|
|
|
function getFrom(key, meId) { |
|
|
|
const isGroupMessage = Boolean(key.participant); |
|
if (isGroupMessage) { |
|
return key.participant; |
|
} |
|
if (key.fromMe) { |
|
return meId || 'me'; |
|
} |
|
return key.remoteJid; |
|
} |
|
|
|
function getDestination(key, meId = undefined): MessageDestination { |
|
return { |
|
id: buildMessageId(key), |
|
to: toCusFormat(getTo(key, meId)), |
|
from: toCusFormat(getFrom(key, meId)), |
|
fromMe: key.fromMe, |
|
}; |
|
} |
|
|