From e1c8d40ecbd2dc4e428b459afab32fe3954b1c2e Mon Sep 17 00:00:00 2001 From: vrajboulder-creator Date: Tue, 7 Jul 2026 16:38:55 +0530 Subject: [PATCH 1/8] feat: @lid history bridge + real history-sync progress Two local patches on top of evolution-api v2.3.7: 1. @lid-history bridge (whatsapp.baileys.service.ts) The messages.upsert (live) path rewrites @lid -> real phone via remoteJidAlt, but messaging-history.set (bulk history) did not, so old @lid chats had no phone/name bridge. Preserve remoteJidAlt on history message keys (from itself or senderPn/participantAlt/ participantPn fallbacks) so a downstream resolver can map an old @lid chat to the saved contact name/number. 2. Real history-sync progress (whatsapp.baileys.service.ts + monitor.service.ts) Baileys emits a real 0-100 `progress` + `isLatest` on messaging-history.set, previously only wired to Chatwoot import. Record it on a public historySync field and surface it on the instanceInfo/fetchInstances response so clients can show a true progress bar instead of inferring from row counts. Co-Authored-By: Claude Opus 4.8 --- .../whatsapp/whatsapp.baileys.service.ts | 39 +++++++++++++++++++ src/api/services/monitor.service.ts | 11 +++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 60e857fcc1..cdc7c853b5 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -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() { @@ -947,6 +958,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; @@ -1043,6 +1063,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)); } diff --git a/src/api/services/monitor.service.ts b/src/api/services/monitor.service.ts index 438530b57e..d016028b7d 100644 --- a/src/api/services/monitor.service.ts +++ b/src/api/services/monitor.service.ts @@ -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) { From 3eeb576e20450efaebbc4ad30ede838a93f8c6ae Mon Sep 17 00:00:00 2001 From: vrajboulder-creator Date: Tue, 7 Jul 2026 16:53:25 +0530 Subject: [PATCH 2/8] fix: logout must always purge creds even when socket already closed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit logoutInstance() called `await this.client.logout()` unguarded. When the WhatsApp socket is already down (user removed the device from their phone, or a dropped connection), client.logout() throws "Connection Closed", which aborted the function BEFORE the credential-purge block that deletes the Baileys auth (Session row). The creds survived, so recreating the same-named instance silently reconnected without a QR — "logout didn't work / still reconnects". Wrap client.logout() (and ws.close()) in try/catch so the credential purge ALWAYS runs. A failed WS logout on an already-closed socket is harmless. Co-Authored-By: Claude Opus 4.8 --- .../whatsapp/whatsapp.baileys.service.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index cdc7c853b5..58881cc076 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -277,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'); const cache = this.configService.get('CACHE'); From 2e20fadcc388b8fd4bbd612e23f8810c15f22687 Mon Sep 17 00:00:00 2001 From: vrajboulder-creator Date: Tue, 7 Jul 2026 17:01:55 +0530 Subject: [PATCH 3/8] feat: on-demand older-history pull (fetchMessageHistory) Add POST /chat/requestHistory/:instance { remoteJid, count? } which calls Baileys' fetchMessageHistory(count, oldestKey, oldestTimestamp) anchored on the oldest stored message of the chat. 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 holds years of other chats. Results arrive async via the messaging-history.set ON_DEMAND path and get persisted. Co-Authored-By: Claude Opus 4.8 --- src/api/controllers/chat.controller.ts | 5 +++ .../whatsapp/whatsapp.baileys.service.ts | 31 +++++++++++++++++++ src/api/routes/chat.router.ts | 12 +++++++ 3 files changed, 48 insertions(+) diff --git a/src/api/controllers/chat.controller.ts b/src/api/controllers/chat.controller.ts index 22e90b9faa..453d3c95e2 100644 --- a/src/api/controllers/chat.controller.ts +++ b/src/api/controllers/chat.controller.ts @@ -58,6 +58,11 @@ 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); + } + public async fetchMessages({ instanceName }: InstanceDto, query: Query) { return await this.waMonitor.waInstances[instanceName].fetchMessages(query); } diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 58881cc076..0d6fd3cafd 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -3888,6 +3888,37 @@ 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' }; + } + } + public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto, getBuffer = false) { try { const m = data?.message; diff --git a/src/api/routes/chat.router.ts b/src/api/routes/chat.router.ts index 158947ed22..1836f6aaa8 100644 --- a/src/api/routes/chat.router.ts +++ b/src/api/routes/chat.router.ts @@ -120,6 +120,18 @@ 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({ + request: req, + schema: null, + ClassRef: Object, + execute: (instance, data) => chatController.requestChatHistory(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({ From b4c0db0d56d42da40ea6e2544f1780bd96940a02 Mon Sep 17 00:00:00 2001 From: vrajboulder-creator Date: Tue, 7 Jul 2026 17:10:10 +0530 Subject: [PATCH 4/8] feat: resolve @lid -> phone via Baileys lidMapping (getPNForLID) Add POST /chat/resolveLid/:instance { lids: string[] } -> { "": "|null" }. Uses signalRepository.lidMapping.getPNForLID to recover the real phone for @lid privacy/business chats that carry no remoteJidAlt in any stored message (so the message-key and picture bridges can't resolve them). Last-resort phone bridge for the "empty contact number" case. Co-Authored-By: Claude Opus 4.8 --- src/api/controllers/chat.controller.ts | 5 ++++ .../whatsapp/whatsapp.baileys.service.ts | 27 +++++++++++++++++++ src/api/routes/chat.router.ts | 12 +++++++++ 3 files changed, 44 insertions(+) diff --git a/src/api/controllers/chat.controller.ts b/src/api/controllers/chat.controller.ts index 453d3c95e2..2c127b7e54 100644 --- a/src/api/controllers/chat.controller.ts +++ b/src/api/controllers/chat.controller.ts @@ -63,6 +63,11 @@ export class ChatController { 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) { return await this.waMonitor.waInstances[instanceName].fetchMessages(query); } diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 0d6fd3cafd..cb1558a573 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -3919,6 +3919,33 @@ export class BaileysStartupService extends ChannelStartupService { } } + // [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 + // { "": "" | null }. Missing/unmapped ids resolve to null. + public async resolveLid({ lids }: { lids: string[] }) { + const out: Record = {}; + 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; diff --git a/src/api/routes/chat.router.ts b/src/api/routes/chat.router.ts index 1836f6aaa8..eae81c68ce 100644 --- a/src/api/routes/chat.router.ts +++ b/src/api/routes/chat.router.ts @@ -132,6 +132,18 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) + // [PATCH lid-resolve] Resolve @lid ids → real phone via Baileys lidMapping. + // POST /chat/resolveLid/:instance { lids: string[] } → { "": "|null" } + .post(this.routerPath('resolveLid'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + 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({ From 946ab74f4358c3283a75ed0f158d2e912f71a1a4 Mon Sep 17 00:00:00 2001 From: vrajboulder-creator Date: Tue, 7 Jul 2026 17:28:33 +0530 Subject: [PATCH 5/8] feat: capture LID<->PN mapping from contacts.upsert (own mapping) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each synced Baileys Contact carries both its phone JID (contact.id = @s.whatsapp.net) and its @lid (contact.lid), but upstream discarded contact.lid — so an @lid chat could never be mapped back to the phone, and thus to the user's saved address-book name (chats showed the contact's own profile name like "." or an emoji instead of "Sahil Boulder"). Now on contacts.upsert we: 1. feed every (lid, pn) pair into Baileys' lidMapping store (signalRepository.lidMapping.storeLIDPNMappings) so getPNForLID() resolves them afterwards, and 2. persist an @lid Contact row mirroring the saved pushName, so the DB name-resolver bridges @lid -> saved name directly. Co-Authored-By: Claude Opus 4.8 --- .../whatsapp/whatsapp.baileys.service.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index cb1558a573..9bf9581448 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -834,6 +834,38 @@ export class BaileysStartupService extends ChannelStartupService { private readonly contactHandle = { 'contacts.upsert': async (contacts: Contact[]) => { try { + // [PATCH lid-map-store] Each synced Contact can carry BOTH its phone JID + // (contact.id = @s.whatsapp.net) AND its @lid (contact.lid). WhatsApp + // delivers this pairing during sync, but upstream discards contact.lid — so + // later we can't map an @lid chat back to the phone (and thus to YOUR saved + // address-book name). Feed every (lid, phone) pair we see into Baileys' + // lidMapping store so getPNForLID() can resolve them afterwards, AND persist + // an @lid Contact row carrying the same saved pushName as the phone row. + try { + const mapping = (this.client as any)?.signalRepository?.lidMapping; + const pairs = contacts + .filter((c: any) => c?.lid && c?.id?.includes('@s.whatsapp.net')) + .map((c: any) => ({ lid: c.lid, pn: c.id })); + if (pairs.length && mapping?.storeLIDPNMappings) { + await mapping.storeLIDPNMappings(pairs); + } + // Also persist an @lid Contact row that mirrors the saved name, so the + // DB name-resolver can bridge @lid → saved name directly. + const lidRows = contacts + .filter((c: any) => c?.lid && (c?.name || c?.verifiedName)) + .map((c: any) => ({ + remoteJid: c.lid, + pushName: c.name || c.verifiedName, + profilePicUrl: null, + instanceId: this.instanceId, + })); + if (lidRows.length && this.configService.get('DATABASE').SAVE_DATA.CONTACTS) { + await this.prismaRepository.contact.createMany({ data: lidRows, skipDuplicates: true }); + } + } catch (e) { + this.logger.warn(['lid-map-store: failed to persist LID↔PN mapping', (e as any)?.message]); + } + const contactsRaw: any = contacts.map((contact) => ({ remoteJid: contact.id, pushName: contact?.name || contact?.verifiedName || contact.id.split('@')[0], From 04bea32aa92ec0c08fd831bfe2b3511fda12c688 Mon Sep 17 00:00:00 2001 From: vrajboulder-creator Date: Tue, 7 Jul 2026 17:30:38 +0530 Subject: [PATCH 6/8] chore: remove upstream CI workflows (build locally, no CI secrets) Evolution's publish_docker_image*.yml workflows try to build + push to Docker Hub on every push and fail (no DOCKERHUB secrets in this fork), spamming Actions failure emails. We build the image locally, so drop the CI entirely. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/check_code_quality.yml | 42 -------------- .github/workflows/publish_docker_image.yml | 50 ----------------- .../publish_docker_image_homolog.yml | 50 ----------------- .../workflows/publish_docker_image_latest.yml | 50 ----------------- .github/workflows/security.yml | 55 ------------------- 5 files changed, 247 deletions(-) delete mode 100644 .github/workflows/check_code_quality.yml delete mode 100644 .github/workflows/publish_docker_image.yml delete mode 100644 .github/workflows/publish_docker_image_homolog.yml delete mode 100644 .github/workflows/publish_docker_image_latest.yml delete mode 100644 .github/workflows/security.yml diff --git a/.github/workflows/check_code_quality.yml b/.github/workflows/check_code_quality.yml deleted file mode 100644 index df156f2117..0000000000 --- a/.github/workflows/check_code_quality.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Check Code Quality - -on: - pull_request: - branches: [ main, develop ] - push: - branches: [ main, develop ] - -jobs: - check-lint-and-build: - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v5 - with: - submodules: recursive - - - name: Install Node - uses: actions/setup-node@v5 - with: - node-version: 20.x - - - name: Cache node modules - uses: actions/cache@v4 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: Install packages - run: npm ci - - - name: Check linting - run: npm run lint:check - - - name: Generate Prisma client - run: npm run db:generate - - - name: Check build - run: npm run build \ No newline at end of file diff --git a/.github/workflows/publish_docker_image.yml b/.github/workflows/publish_docker_image.yml deleted file mode 100644 index c5a3996edb..0000000000 --- a/.github/workflows/publish_docker_image.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Build Docker image - -on: - push: - tags: - - "*.*.*" - -jobs: - build_deploy: - name: Build and Deploy - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Checkout - uses: actions/checkout@v5 - with: - submodules: recursive - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: evoapicloud/evolution-api - tags: type=semver,pattern=v{{version}} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push - id: docker_build - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} \ No newline at end of file diff --git a/.github/workflows/publish_docker_image_homolog.yml b/.github/workflows/publish_docker_image_homolog.yml deleted file mode 100644 index a76e1008be..0000000000 --- a/.github/workflows/publish_docker_image_homolog.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Build Docker image - -on: - push: - branches: - - develop - -jobs: - build_deploy: - name: Build and Deploy - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Checkout - uses: actions/checkout@v5 - with: - submodules: recursive - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: evoapicloud/evolution-api - tags: homolog - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push - id: docker_build - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/publish_docker_image_latest.yml b/.github/workflows/publish_docker_image_latest.yml deleted file mode 100644 index f73fe80e4e..0000000000 --- a/.github/workflows/publish_docker_image_latest.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Build Docker image - -on: - push: - branches: - - main - -jobs: - build_deploy: - name: Build and Deploy - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Checkout - uses: actions/checkout@v5 - with: - submodules: recursive - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: evoapicloud/evolution-api - tags: latest - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push - id: docker_build - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml deleted file mode 100644 index b83f0b5d98..0000000000 --- a/.github/workflows/security.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Security Scan - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main, develop ] - schedule: - - cron: '0 0 * * 1' # Weekly on Mondays - -jobs: - codeql: - name: CodeQL Analysis - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - submodules: recursive - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" - - dependency-review: - name: Dependency Review - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - steps: - - name: Checkout Repository - uses: actions/checkout@v5 - with: - submodules: recursive - - name: Dependency Review - uses: actions/dependency-review-action@v4 From 1d469e629e74403b37d4df4357a46bc2295c8a0c Mon Sep 17 00:00:00 2001 From: vrajboulder-creator Date: Tue, 7 Jul 2026 17:36:18 +0530 Subject: [PATCH 7/8] fix: strip :device suffix in resolveLid phone extraction getPNForLID returns the PN as ':0@s.whatsapp.net'. Stripping only '@...' left the ':0' whose colon got removed by replace(/\D/g), producing a spurious trailing '0' (919408740590 -> 9194087405900). Split on ':' too. Co-Authored-By: Claude Opus 4.8 --- .../channel/whatsapp/whatsapp.baileys.service.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 9bf9581448..f5dbd98dad 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -3963,13 +3963,17 @@ export class BaileysStartupService extends ChannelStartupService { 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; + // Already a phone JID (or bare number) → strip to digits (drop @domain + // AND the :device suffix, e.g. "919408740590:0@s.whatsapp.net"). + out[lid] = lid.split('@')[0].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, '') : ''; + // getPNForLID returns e.g. "919408740590:0@s.whatsapp.net" — split on BOTH + // '@' and ':' so the trailing device index (":0") doesn't leave a spurious + // extra digit on the phone number. + const digits = pn ? String(pn).split('@')[0].split(':')[0].replace(/\D/g, '') : ''; out[lid] = digits || null; } catch { out[lid] = null; From e16762e0a5873f232dc6d8f537975fa841eb8241 Mon Sep 17 00:00:00 2001 From: vrajboulder-creator Date: Tue, 7 Jul 2026 17:52:54 +0530 Subject: [PATCH 8/8] feat: resolveLid deep message scan for unsaved @lid numbers When the live lidMapping has no PN for an @lid, fall back to ONE raw SQL over the chat's entire stored history, checking every field WhatsApp can leak the phone in: key.remoteJidAlt / senderPn / participantAlt / participantPn and contextInfo.participant. Also reject junk (<8 digit) results so a bare device index never renders as '+0'. Co-Authored-By: Claude Opus 4.8 --- .../whatsapp/whatsapp.baileys.service.ts | 57 ++++++++++++++----- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index f5dbd98dad..2d526f1c53 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -3951,33 +3951,60 @@ export class BaileysStartupService extends ChannelStartupService { } } - // [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 - // { "": "" | null }. Missing/unmapped ids resolve to null. + // [PATCH lid-resolve] Resolve one or more @lid privacy ids to their REAL phone. + // Two bridges, in order: + // 1. Baileys' live LID↔PN mapping (signalRepository.lidMapping.getPNForLID) + // 2. DEEP message scan — search EVERY stored message of the chat for any + // field that can carry the phone (key.remoteJidAlt / senderPn / + // participantAlt / participantPn, contextInfo.participant). WhatsApp leaks + // the PN in different places depending on message age/direction, so one + // SQL over the whole history recovers numbers the live mapping misses. + // Returns { "": "" | null }. public async resolveLid({ lids }: { lids: string[] }) { const out: Record = {}; const mapping = (this.client as any)?.signalRepository?.lidMapping; + const clean = (jid: any): string | null => { + const digits = jid ? String(jid).split('@')[0].split(':')[0].replace(/\D/g, '') : ''; + return digits.length >= 8 && digits.length <= 15 ? digits : null; // reject "0"/junk + }; 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) → strip to digits (drop @domain - // AND the :device suffix, e.g. "919408740590:0@s.whatsapp.net"). - out[lid] = lid.split('@')[0].split(':')[0].replace(/\D/g, '') || null; + out[lid] = clean(lid); // already a phone JID / bare number continue; } + // Bridge 1: live Baileys mapping. + let phone: string | null = null; try { const pn = mapping?.getPNForLID ? await mapping.getPNForLID(lid) : null; - // getPNForLID returns e.g. "919408740590:0@s.whatsapp.net" — split on BOTH - // '@' and ':' so the trailing device index (":0") doesn't leave a spurious - // extra digit on the phone number. - const digits = pn ? String(pn).split('@')[0].split(':')[0].replace(/\D/g, '') : ''; - out[lid] = digits || null; - } catch { - out[lid] = null; + phone = clean(pn); + } catch { /* mapping miss */ } + // Bridge 2: deep scan of the chat's stored messages for any PN-carrying field. + if (!phone) { + try { + const rows: any[] = await this.prismaRepository.$queryRaw` + SELECT COALESCE( + NULLIF(split_part("key"->>'remoteJidAlt','@',1), ''), + NULLIF(split_part("key"->>'senderPn','@',1), ''), + NULLIF(split_part("key"->>'participantAlt','@',1), ''), + NULLIF(split_part("key"->>'participantPn','@',1), ''), + NULLIF(split_part("contextInfo"->>'participant','@',1), '') + ) AS phone + FROM "Message" + WHERE "instanceId" = ${this.instanceId} AND "key"->>'remoteJid' = ${lid} + AND ( + "key"->>'remoteJidAlt' LIKE '%@s.whatsapp.net' OR + "key"->>'senderPn' LIKE '%@s.whatsapp.net' OR + "key"->>'participantAlt' LIKE '%@s.whatsapp.net' OR + "key"->>'participantPn' LIKE '%@s.whatsapp.net' OR + "contextInfo"->>'participant' LIKE '%@s.whatsapp.net' + ) + LIMIT 1`; + phone = clean(rows?.[0]?.phone); + } catch { /* raw scan unavailable → leave null */ } } + out[lid] = phone; } return out; }