Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/api/controllers/chat.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ export class ChatController {
return await this.waMonitor.waInstances[instanceName].getBase64FromMediaMessage(data);
}

// [PATCH fetch-history] Request older history for a chat (on-demand pull).
public async requestChatHistory({ instanceName }: InstanceDto, data: { remoteJid: string; count?: number }) {
return await this.waMonitor.waInstances[instanceName].requestChatHistory(data);
}

// [PATCH lid-resolve] Resolve @lid ids β†’ real phone via Baileys lidMapping.
public async resolveLid({ instanceName }: InstanceDto, data: { lids: string[] }) {
return await this.waMonitor.waInstances[instanceName].resolveLid(data);
}

public async fetchMessages({ instanceName }: InstanceDto, query: Query<Message>) {
return await this.waMonitor.waInstances[instanceName].fetchMessages(query);
}
Expand Down
116 changes: 114 additions & 2 deletions src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,17 @@ export class BaileysStartupService extends ChannelStartupService {

public stateConnection: wa.StateConnection = { state: 'close' };

// [PATCH sync-progress] Live WhatsApp history-sync progress, updated from the
// messaging-history.set handler (Baileys emits a real 0-100 `progress` + a final
// `isLatest`). Surfaced on the instanceInfo/fetchInstances response so a client
// can show a true progress bar instead of inferring from row counts.
public historySync: { progress: number; isLatest: boolean; syncing: boolean; updatedAt: number } = {
progress: 0,
isLatest: false,
syncing: false,
updatedAt: 0,
};

public phoneNumber: string;

public get connectionStatus() {
Expand All @@ -266,9 +277,24 @@ export class BaileysStartupService extends ChannelStartupService {

public async logoutInstance() {
this.messageProcessor.onDestroy();
await this.client?.logout('Log out instance: ' + this.instanceName);
// [PATCH logout-purge] client.logout() THROWS "Connection Closed" when the
// socket is already down (e.g. the user removed the device from their PHONE).
// Previously that error aborted logoutInstance BEFORE the credential purge
// below ran, so the Baileys auth creds survived and re-creating the (same,
// deterministic) instance name silently reconnected β€” "logout didn't work".
// Swallow logout errors so the purge ALWAYS runs; a failed WS logout on an
// already-closed socket is harmless.
try {
await this.client?.logout('Log out instance: ' + this.instanceName);
} catch (err) {
this.logger.warn(['logoutInstance: client.logout failed (already closed?)', (err as any)?.message]);
}

this.client?.ws?.close();
try {
this.client?.ws?.close();
} catch {
/* socket already closed */
}

const db = this.configService.get<Database>('DATABASE');
const cache = this.configService.get<CacheConf>('CACHE');
Expand Down Expand Up @@ -947,6 +973,15 @@ export class BaileysStartupService extends ChannelStartupService {
`recv ${chats.length} chats, ${contacts.length} contacts, ${messages.length} msgs (is latest: ${isLatest}, progress: ${progress}%), type: ${syncType}`,
);

// [PATCH sync-progress] Record the real Baileys history-sync progress so
// the instanceInfo response can surface a true % (syncing until isLatest).
this.historySync = {
progress: typeof progress === 'number' ? progress : this.historySync.progress,
isLatest: !!isLatest,
syncing: !isLatest,
updatedAt: Date.now(),
};

const instance: InstanceDto = { instanceName: this.instance.name };

let timestampLimitToImport = null;
Expand Down Expand Up @@ -1043,6 +1078,25 @@ export class BaileysStartupService extends ChannelStartupService {
}
}

// [PATCH @lid-history] Bridge old @lid history chats to the real phone.
// We do NOT rewrite key.remoteJid (the Chat row is still keyed by @lid, so
// rewriting would orphan the message from its chat). Instead we ensure the
// real-phone mapping is PRESERVED on the stored key as remoteJidAlt, which
// is exactly what the downstream name-resolver reads to map @lid β†’ your
// saved contact name/number. Baileys usually populates remoteJidAlt on the
// key already; when it instead carries the phone in senderPn/participantAlt
// (older shapes) we copy it across so history rows match the live path.
if (m.key?.remoteJid?.includes('@lid') && !m.key?.remoteJidAlt) {
const altPhone =
(m.key as any).senderPn ||
(m.key as any).participantAlt ||
(m.key as any).participantPn ||
null;
if (altPhone && String(altPhone).includes('@s.whatsapp.net')) {
m.key.remoteJidAlt = altPhone;
}
}

messagesRaw.push(this.prepareMessage(m));
}

Expand Down Expand Up @@ -3834,6 +3888,64 @@ export class BaileysStartupService extends ChannelStartupService {
return map[mediaType] || null;
}

// [PATCH fetch-history] On-demand older-history pull. WhatsApp's initial
// multi-device sync only delivers a bounded window per chat, so old messages of
// a specific chat can be missing even when the DB has years of other chats.
// fetchMessageHistory(count, oldestKey, oldestTimestamp) asks WhatsApp for
// messages OLDER than the given anchor; the results arrive asynchronously via
// the normal messaging-history.set handler (ON_DEMAND syncType) and get saved
// like any other history. We anchor on the OLDEST message we currently have for
// the chat. Call repeatedly (each call walks further back) until no new older
// messages arrive. Returns { requested, oldestTimestamp } or { requested:false }
// when there's nothing to anchor on.
public async requestChatHistory({ remoteJid, count }: { remoteJid: string; count?: number }) {
const want = Math.min(Math.max(Number(count) || 50, 1), 50); // Baileys caps at 50/req
// Find the oldest stored message for this chat to use as the "before" anchor.
const oldest = await this.prismaRepository.message.findFirst({
where: { instanceId: this.instanceId, key: { path: ['remoteJid'], equals: remoteJid } },
orderBy: { messageTimestamp: 'asc' },
});
if (!oldest) {
return { requested: false, reason: 'no messages stored for this chat to anchor on' };
}
const key = oldest.key as any;
const ts = Number(oldest.messageTimestamp);
try {
const requestId = await this.client.fetchMessageHistory(want, key, ts);
return { requested: true, requestId, anchorTimestamp: ts, anchorMessageId: key?.id ?? null };
} catch (err) {
this.logger.error(['requestChatHistory failed', (err as any)?.message]);
return { requested: false, reason: (err as any)?.message ?? 'fetchMessageHistory failed' };
}
}

// [PATCH lid-resolve] Resolve one or more @lid privacy ids to their REAL phone
// JID using Baileys' live LID↔PN mapping (signalRepository.lidMapping). This is
// the bridge that recovers the phone for @lid chats that carry no remoteJidAlt
// in stored messages (business / privacy contacts). Returns a map
// { "<lid>": "<phone digits>" | null }. Missing/unmapped ids resolve to null.
public async resolveLid({ lids }: { lids: string[] }) {
const out: Record<string, string | null> = {};
const mapping = (this.client as any)?.signalRepository?.lidMapping;
for (const raw of Array.isArray(lids) ? lids : []) {
const lid = String(raw || '');
if (!lid) continue;
if (!lid.endsWith('@lid')) {
// Already a phone JID (or bare number) β†’ just strip to digits.
out[lid] = lid.split('@')[0].replace(/\D/g, '') || null;
continue;
}
try {
const pn = mapping?.getPNForLID ? await mapping.getPNForLID(lid) : null;
const digits = pn ? String(pn).split('@')[0].replace(/\D/g, '') : '';
out[lid] = digits || null;
} catch {
out[lid] = null;
}
}
return out;
}

public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto, getBuffer = false) {
try {
const m = data?.message;
Expand Down
24 changes: 24 additions & 0 deletions src/api/routes/chat.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,30 @@ export class ChatRouter extends RouterBroker {

return res.status(HttpStatus.CREATED).json(response);
})
// [PATCH fetch-history] On-demand older-history pull for a chat.
// POST /chat/requestHistory/:instance { remoteJid, count? }
.post(this.routerPath('requestHistory'), ...guards, async (req, res) => {
const response = await this.dataValidate<any>({
request: req,
schema: null,
ClassRef: Object,
execute: (instance, data) => chatController.requestChatHistory(instance, data),
});

return res.status(HttpStatus.OK).json(response);
})
// [PATCH lid-resolve] Resolve @lid ids β†’ real phone via Baileys lidMapping.
// POST /chat/resolveLid/:instance { lids: string[] } β†’ { "<lid>": "<phone>|null" }
.post(this.routerPath('resolveLid'), ...guards, async (req, res) => {
const response = await this.dataValidate<any>({
request: req,
schema: null,
ClassRef: Object,
execute: (instance, data) => chatController.resolveLid(instance, data),
});

return res.status(HttpStatus.OK).json(response);
})
// TODO: corrigir updateMessage para medias tambem
.post(this.routerPath('updateMessage'), ...guards, async (req, res) => {
const response = await this.dataValidate<UpdateMessageDto>({
Expand Down
11 changes: 10 additions & 1 deletion src/api/services/monitor.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,16 @@ export class WAMonitoringService {
},
});

return instances;
// [PATCH sync-progress] Attach the live in-memory history-sync progress (real
// Baileys 0-100 %) from the running instance to each row, so clients get a
// true progress bar without inferring from row counts.
return instances.map((instance) => {
const live = this.waInstances[instance.name] as any;
return {
...instance,
historySync: live?.historySync ?? { progress: 0, isLatest: false, syncing: false, updatedAt: 0 },
};
});
}

public async instanceInfoById(instanceId?: string, number?: string) {
Expand Down