Compare commits

...
17 Commits
Author SHA1 Message Date
devlikepro 41b669a35c [core] 2025.10.4
Release / WEBJS - chrome - amd64 - chrome (push) Canceled after 0s
Release / WEBJS - chromium - amd64 - latest (push) Canceled after 0s
Release / WEBJS - chromium - linux/arm64 - arm (push) Canceled after 0s
Release / GOWS - none - amd64 - gows (push) Canceled after 0s
Release / GOWS - none - linux/arm64 - gows-arm (push) Canceled after 0s
Release / NOWEB - none - amd64 - noweb (push) Canceled after 0s
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Canceled after 0s
2025-10-19 17:08:06 +07:00
devlikepro 7b5ca3bc57 [core] Up Dashboard 2025-10-19 17:08:06 +07:00
devlikepro bb0c7fd22b [core] NOWEB, GOWS - better handle message.edited
fix #1394
2025-10-19 17:08:05 +07:00
devlikepro c79958541e [core] GOWS - use proto type 2025-10-19 17:08:05 +07:00
devlikepro 94ee8863d6 [core] Up Dashboard - headers, agent name 2025-10-19 17:08:04 +07:00
devlikepro 011477262d [core] Up NOWEB
fix #1538
2025-10-19 17:08:04 +07:00
devlikeproandbergpinheiro 2e8e4cefb0 [core] Fixes inverted logic of messages read in ChatWoot
closes devlikeapro/waha-plus#20

Adapted from: bergpinheiro/read-chatwoot-invertida@ef2ff1b9de

(cherry picked from commit ef2ff1b9deceeea3c26747531e26d66e96f931fd)
(cherry picked from commit 3dc3440d2d5077279934577007f89cd640f60d9e)

Co-authored-by: bergpinheiro <bergpinheiro@users.noreply.github.com>"
2025-10-19 17:08:04 +07:00
devlikepro 5e2802c098 [core] metadata is Record 2025-10-19 17:08:04 +07:00
devlikepro 9a6250a055 [core] Add jid to MeInfo type 2025-10-19 17:08:03 +07:00
devlikepro f8cb56525a [core] has recent 2025-10-19 17:08:03 +07:00
devlikepro b5b491cb7b [core] en in waha 2025-10-19 17:08:02 +07:00
devlikepro 7eb6be70f9 [core] dev image 2025-10-19 17:08:02 +07:00
devlikepro 0de8a5fabc [core] NOWEB - Fix sending media to channels
fix #1523
2025-10-19 17:08:02 +07:00
Berg Pinheiro 9fbc18fb70 [core] Fix URL breaking in ChatWoot integration
- Fix WhatsappToMarkdown function to preserve URLs with underscores
- URLs are now protected before markdown transformations
- Prevents Sarah_Jessica_Parker from becoming Sarah*Jessica*Parker
- Add comprehensive tests for URL preservation scenarios
- Maintains existing markdown formatting for non-URL text
- All comments in English for code consistency

Fixes: Links with underscores being broken in ChatWoot messages

fix devlikeapro/waha#1522
closes devlikeapro/waha-plus#19
2025-10-19 17:08:02 +07:00
devlikepro e7ce3368b0 [core] Fix tests 2025-10-19 17:08:02 +07:00
devlikepro 729600e9c6 [core] voice 2025-10-17 11:44:55 +07:00
devlikepro 54886f385b [core] Disable pt 2025-10-17 10:41:35 +07:00
26 changed files with 872 additions and 133 deletions

No files matched your search

+126
View File
@@ -0,0 +1,126 @@
name: Dev
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
docker-build:
if: github.repository == 'devlikeapro/waha-plus'
runs-on: ${{ matrix.runner }}
name:
${{ matrix.engine }} - ${{ matrix.browser }} - ${{ matrix.platform }} -
${{ matrix.tag }}
strategy:
matrix:
include:
- runner: 'buildjet-4vcpu-ubuntu-2204'
tag: 'dev'
platform: 'amd64'
browser: 'chromium'
engine: 'WEBJS'
goss: 'goss-linux-amd64'
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
tag: 'dev-arm'
platform: 'linux/arm64'
browser: 'chromium'
engine: 'WEBJS'
goss: 'goss-linux-arm'
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Check recent changes
id: recent
run: |
count=$(git rev-list --after="24 hours ago" --count HEAD || true)
if [ "$count" -gt 0 ]; then
echo "has_recent=true" >> "$GITHUB_OUTPUT"
else
echo "has_recent=false" >> "$GITHUB_OUTPUT"
fi
- name: No recent changes summary
if: ${{ steps.recent.outputs.has_recent != 'true' }}
run:
echo "No commits in the last 24 hours; skipping build." >>
"$GITHUB_STEP_SUMMARY"
- name: Set up Docker Buildx
if: ${{ steps.recent.outputs.has_recent == 'true' }}
uses: docker/setup-buildx-action@v2
- name: Docker meta
if: ${{ steps.recent.outputs.has_recent == 'true' }}
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ vars.DOCKER_IMAGE }}
flavor: latest=false
tags: |
type=raw,value=${{ matrix.tag }}
- name: Login to image repository
if: ${{ steps.recent.outputs.has_recent == 'true' }}
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build
if: ${{ steps.recent.outputs.has_recent == 'true' }}
uses: docker/build-push-action@v4
with:
context: .
file: Dockerfile
platforms: ${{ matrix.platform }}
build-args: |
USE_BROWSER=${{ matrix.browser }}
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
push: false
load: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Install goss
if: ${{ steps.recent.outputs.has_recent == 'true' }}
env:
GOSS_DOWNLOAD: 'https://github.com/goss-org/goss/releases/download'
GOSS_VERSION: 'v0.4.8'
run: |
curl -L https://github.com/goss-org/goss/releases/download/${{ env.GOSS_VERSION }}/${{ matrix.goss }} -o /usr/local/bin/goss
chmod +rx /usr/local/bin/goss
- name: Run smoke tests
if: ${{ steps.recent.outputs.has_recent == 'true' }}
working-directory: tests/smoke
timeout-minutes: 2
run: |
docker run -d \
--name smoke \
--rm -p3000:3000 \
-e WAHA_API_KEY=test \
${{ vars.DOCKER_IMAGE }}:${{ matrix.tag }}
sleep 3
docker logs smoke
goss validate --retry-timeout 30s --sleep 1s
docker rm --force smoke
- name: Push
if: ${{ steps.recent.outputs.has_recent == 'true' }}
uses: docker/build-push-action@v4
with:
context: .
file: Dockerfile
platforms: ${{ matrix.platform }}
build-args: |
USE_BROWSER=${{ matrix.browser }}
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+18 -18
View File
@@ -15,6 +15,7 @@ on:
jobs:
en:
if: github.repository == 'devlikeapro/waha'
permissions:
issues: write
discussions: write
@@ -31,21 +32,20 @@ jobs:
CUSTOM_BOT_NOTE: 'English'
TARGET_LANGUAGE: 'eng'
TRANSLATED_LANGUAGE: 'en'
pt:
permissions:
issues: write
discussions: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: devlikeapro/github-translate-action@main
env:
# GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
IS_MODIFY_TITLE: false
CUSTOM_BOT_NOTE: 'Portuguese'
TARGET_LANGUAGE: 'por'
TRANSLATED_LANGUAGE: 'pt'
# pt:
# permissions:
# issues: write
# discussions: write
# pull-requests: write
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - uses: devlikeapro/github-translate-action@main
# env:
# # GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# IS_MODIFY_TITLE: false
# CUSTOM_BOT_NOTE: 'Portuguese'
# TARGET_LANGUAGE: 'por'
# TRANSLATED_LANGUAGE: 'pt'
+1 -1
View File
@@ -35,7 +35,7 @@ for-swagger:
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true . ${NVM_DIR}/nvm.sh && nvm exec yarn start
up-noweb:
yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2025-10-10
yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2025-10-19
up-noweb-libsignal:
yarn up libsignal@github:devlikeapro/libsignal-node#fork-master
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -26,7 +26,7 @@
"gows:proto": "yarn gows:proto:fetch && yarn gows:proto:build"
},
"dependencies": {
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2025-10-10",
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2025-10-19",
"@adiwajshing/keyed-db": "^0.2.4",
"@aws-sdk/client-s3": "^3.633.0",
"@aws-sdk/s3-request-presigner": "^3.633.0",
@@ -13,13 +13,11 @@ import { Locale } from '@waha/apps/chatwoot/i18n/locale';
import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { WAHAEvents, WAMessageAck } from '@waha/structures/enums.dto';
import { WAHAEvents } from '@waha/structures/enums.dto';
import { WAHAWebhookMessageAck } from '@waha/structures/webhooks.dto';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { isJidCusFormat } from '@waha/utils/wa';
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import { toCusFormat } from '@waha/core/utils/jids';
import { ShouldMarkAsReadInChatWoot } from '@waha/apps/chatwoot/consumers/waha/message.ack.utils';
@Processor(QueueName.WAHA_MESSAGE_ACK, { concurrency: JOB_CONCURRENCY })
export class WAHAMessageAckConsumer extends ChatWootWAHABaseConsumer {
@@ -31,30 +29,8 @@ export class WAHAMessageAckConsumer extends ChatWootWAHABaseConsumer {
super(manager, log, rmutex, 'WAHAMessageAckConsumer');
}
ShouldProcess(event: WAHAWebhookMessageAck): boolean {
// Mark as seen only if it's DM
// Ignore groups and other multiple participants chats
const chatId = this.GetChatId(event);
if (!isJidCusFormat(chatId) && !isLidUser(chatId)) {
return false;
}
const payload = event.payload;
// Only READ and PLAYED
const read = payload.ack === WAMessageAck.READ;
const played = payload.ack == WAMessageAck.PLAYED;
if (!read && !played) {
return true;
}
// Only not from me
const sender = toCusFormat(payload._data?.Sender) || payload.from;
const me = this.manager.getSession(event.session).getSessionMeInfo();
if (sender == me.id || sender === me.lid) {
this.logger.debug('Ignore from me ack');
// Ignore from me
return false;
}
return true;
ShouldProcess(event: any): boolean {
return ShouldMarkAsReadInChatWoot(event);
}
GetChatId(event: WAHAWebhookMessageAck): string {
@@ -0,0 +1,466 @@
import type { WAHAEngine, WAHAEvents } from '@waha/structures/enums.dto';
import { ShouldMarkAsReadInChatWoot } from '@waha/apps/chatwoot/consumers/waha/message.ack.utils';
interface TestParam {
name: string;
expected: boolean;
GOWS: any;
NOWEB: any;
WEBJS: any;
}
/**
* Me - 444444444444 (lid - 144444444444444)
* Participant - 11111111111 (lid - 011111111111111)
* Group - 222222222222222222
*/
const ShouldMarkAsReadCases: TestParam[] = [
{
name: 'Participant read My message (READ ack)',
expected: true,
GOWS: {
id: 'evt_00000000000000000000000000',
session: 'default',
event: 'message.ack' as WAHAEvents,
payload: {
id: 'true_11111111111@c.us_3333333333333333333333',
from: '11111111111@c.us',
to: null,
participant: null,
fromMe: true,
ack: 3,
ackName: 'READ',
_data: {
Chat: '11111111111@s.whatsapp.net',
Sender: '11111111111@s.whatsapp.net',
IsFromMe: false,
IsGroup: false,
AddressingMode: '',
SenderAlt: '',
RecipientAlt: '',
BroadcastListOwner: '',
BroadcastRecipients: null,
MessageIDs: ['3333333333333333333333'],
Type: 'read',
MessageSender: '',
},
},
metadata: {},
me: {
id: '444444444444@c.us',
lid: '144444444444444@lid',
jid: '444444444444:42@s.whatsapp.net',
pushName: '',
},
environment: {
version: '2025.10.4',
engine: 'GOWS',
tier: 'PLUS',
browser: null,
},
engine: 'GOWS' as WAHAEngine,
},
NOWEB: {
id: 'evt_00000000000000000000000000',
session: 'default',
event: 'message.ack',
payload: {
id: 'true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
from: '11111111111@c.us',
fromMe: true,
ack: 3,
ackName: 'READ',
},
metadata: {},
me: {
id: '444444444444@c.us',
pushName: '',
lid: '144444444444444@lid',
},
engine: 'NOWEB',
environment: {
version: '2025.10.4',
engine: 'NOWEB',
tier: 'PLUS',
browser: null,
},
},
WEBJS: {
id: 'evt_00000000000000000000000000',
session: 'default',
event: 'message.ack',
payload: {
id: 'true_11111111111@c.us_AAA',
from: '444444444444@c.us',
fromMe: true,
source: 'app',
to: '11111111111@c.us',
body: 'Nn',
hasMedia: false,
media: null,
ack: 3,
ackName: 'READ',
vCards: [],
_data: {
id: {
fromMe: true,
remote: '11111111111@c.us',
id: 'AAA',
_serialized: 'true_11111111111@c.us_AAA',
},
viewed: false,
from: '444444444444@c.us',
to: '11111111111@c.us',
author: '444444444444@c.us',
ack: 3,
},
},
metadata: {},
me: {
id: '444444444444@c.us',
pushName: '',
},
engine: 'WEBJS',
environment: {
version: '2025.10.4',
engine: 'WEBJS',
tier: 'PLUS',
browser: '/usr/bin/google-chrome-stable',
},
},
},
{
name: 'Participant received My message (DEVICE ack)',
expected: false,
GOWS: {
id: 'evt_00000000000000000000000000',
session: 'default',
event: 'message.ack' as WAHAEvents,
payload: {
id: 'true_11111111111@c.us_3333333333333333333333',
from: '11111111111@c.us',
to: null,
participant: null,
fromMe: true,
ack: 2,
ackName: 'DEVICE',
_data: {
Chat: '11111111111@s.whatsapp.net',
Sender: '11111111111:3@s.whatsapp.net',
IsFromMe: false,
IsGroup: false,
AddressingMode: '',
SenderAlt: '',
RecipientAlt: '',
BroadcastListOwner: '',
BroadcastRecipients: null,
MessageIDs: ['3333333333333333333333'],
Type: '',
MessageSender: '',
},
},
me: {
id: '444444444444@c.us',
lid: '144444444444444@lid',
jid: '444444444444:42@s.whatsapp.net',
pushName: '',
},
environment: {
version: '2025.10.4',
engine: 'GOWS',
tier: 'PLUS',
browser: null,
},
engine: 'GOWS' as WAHAEngine,
},
NOWEB: {
id: 'evt_00000000000000000000000000',
session: 'default',
event: 'message.ack',
payload: {
id: 'true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
from: '11111111111@c.us',
fromMe: true,
ack: 2,
ackName: 'DEVICE',
},
metadata: {},
me: {
id: '444444444444@c.us',
pushName: '',
lid: '1144444444444444@lid',
},
engine: 'NOWEB',
environment: {
version: '2025.10.4',
engine: 'NOWEB',
tier: 'PLUS',
browser: null,
},
},
WEBJS: {
id: 'evt_00000000000000000000000000',
session: 'default',
event: 'message.ack',
payload: {
id: 'true_11111111111@c.us_AAA',
from: '444444444444@c.us',
fromMe: true,
source: 'app',
to: '11111111111@c.us',
body: 'Nn',
hasMedia: false,
media: null,
ack: 2,
ackName: 'DEVICE',
vCards: [],
_data: {
id: {
fromMe: true,
remote: '11111111111@c.us',
id: 'AAA',
_serialized: 'true_11111111111@c.us_AAA',
},
from: '444444444444@c.us',
to: '11111111111@c.us',
author: '444444444444@c.us',
ack: 2,
},
},
metadata: {},
me: {
id: '444444444444@c.us',
pushName: '',
},
engine: 'WEBJS',
environment: {
version: '2025.10.4',
engine: 'WEBJS',
tier: 'PLUS',
browser: '/usr/bin/google-chrome-stable',
},
},
},
{
name: 'Me read Participant message on another device (READ ack)',
expected: false,
GOWS: {
id: 'evt_00000000000000000000000000',
session: 'default',
event: 'message.ack' as WAHAEvents,
payload: {
id: 'false_11111111111@c.us_3333333333333333333333',
from: '11111111111@c.us',
to: null,
participant: null,
fromMe: false,
ack: 3,
ackName: 'READ',
_data: {
Chat: '11111111111@s.whatsapp.net',
Sender: '444444444444@s.whatsapp.net',
IsFromMe: true,
IsGroup: false,
AddressingMode: '',
SenderAlt: '',
RecipientAlt: '',
BroadcastListOwner: '',
BroadcastRecipients: null,
MessageIDs: ['3333333333333333333333'],
Type: 'read',
MessageSender: '11111111111@s.whatsapp.net',
},
},
metadata: {},
me: {
id: '444444444444@c.us',
lid: '144444444444444@lid',
jid: '444444444444:42@s.whatsapp.net',
pushName: '',
},
environment: {
version: '2025.10.4',
engine: 'GOWS',
tier: 'PLUS',
browser: null,
},
engine: 'GOWS' as WAHAEngine,
},
NOWEB: {
id: 'evt_00000000000000000000000000',
session: 'default',
event: 'message.ack',
payload: {
id: '11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
from: '11111111111@c.us',
fromMe: false,
ack: 3,
ackName: 'READ',
},
metadata: {},
me: {
id: '444444444444@c.us',
pushName: '',
lid: '1144444444444444@lid',
},
engine: 'NOWEB',
environment: {
version: '2025.10.4',
engine: 'NOWEB',
tier: 'PLUS',
browser: null,
},
},
WEBJS: null,
},
{
name: 'Participant read My Group Message (READ ack)',
expected: false,
GOWS: {
id: 'evt_00000000000000000000000000',
session: 'default',
payload: {
id: 'true_222222222222222222@g.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_444444444444@c.us',
from: '222222222222222222@g.us',
to: '011111111111111@lid',
participant: '011111111111111@lid',
fromMe: true,
ack: 3,
ackName: 'READ',
_data: {
Chat: '222222222222222222@g.us',
Sender: '011111111111111@lid',
IsFromMe: false,
IsGroup: true,
AddressingMode: '',
SenderAlt: '',
RecipientAlt: '',
BroadcastListOwner: '',
BroadcastRecipients: null,
MessageIDs: ['AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'],
Type: 'read',
MessageSender: '',
},
},
metadata: {},
me: {
id: '444444444444@c.us',
lid: '144444444444444@lid',
jid: '444444444444:42@s.whatsapp.net',
pushName: '',
},
environment: {
version: '2025.10.4',
engine: 'GOWS',
tier: 'PLUS',
browser: null,
},
},
NOWEB: {
id: 'evt_00000000000000000000000000',
session: 'default',
event: 'message.ack',
payload: {
id: 'true_222222222222222222@g.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_444444444444@c.us',
from: '222222222222222222@g.us',
to: '222222222222222222@g.us',
participant: '011111111111111@lid',
fromMe: true,
ack: 3,
ackName: 'READ',
_data: {
key: {
remoteJid: '222222222222222222@g.us',
id: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
fromMe: true,
participant: '011111111111111@lid',
},
receipt: {
userJid: '011111111111111@lid',
},
},
},
metadata: {},
me: {
id: '444444444444@c.us',
pushName: '',
lid: '1144444444444444@lid',
},
engine: 'NOWEB',
environment: {
version: '2025.10.4',
engine: 'NOWEB',
tier: 'PLUS',
browser: null,
},
},
WEBJS: {
id: 'evt_00000000000000000000000000',
session: 'default',
event: 'message.ack',
payload: {
id: 'true_222222222222222222@g.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_444444444444@c.us',
from: '222222222222222222@g.us',
to: '222222222222222222@g.us',
participant: '011111111111111@lid',
fromMe: false,
ack: 3,
ackName: 'READ',
_data: {
tag: 'receipt',
attrs: {
from: {
$1: {
type: 0,
user: '222222222222222222',
server: 'g.us',
},
},
type: 'read',
id: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
participant: {
$1: {
type: 1,
user: '011111111111111',
device: 0,
domainType: 1,
},
},
},
content: null,
},
},
metadata: {},
me: {
id: '444444444444@c.us',
pushName: '',
},
engine: 'WEBJS',
environment: {
version: '2025.10.4',
engine: 'WEBJS',
tier: 'PLUS',
browser: '/usr/bin/google-chrome-stable',
},
},
},
];
describe('ShouldMarkAsReadInChatWoot', () => {
const engines = ['GOWS', 'NOWEB', 'WEBJS'];
for (const engine of engines) {
describe(engine, () => {
for (const param of ShouldMarkAsReadCases) {
const name = `[${param.expected}] ${param.name}`;
const event = param[engine];
const testfn = event ? test : test.skip;
const expected = param.expected;
testfn(name, () => {
// Test
const result = ShouldMarkAsReadInChatWoot(event);
expect(result).toBe(expected);
});
}
});
}
});
@@ -0,0 +1,31 @@
import type { WAHAWebhookMessageAck } from '@waha/structures/webhooks.dto';
import { isJidCusFormat } from '@waha/utils/wa';
import { WAMessageAck } from '@waha/structures/enums.dto';
import { isLidUser, toCusFormat } from '@waha/core/utils/jids';
export function ShouldMarkAsReadInChatWoot(
event: WAHAWebhookMessageAck,
): boolean {
// Mark as seen only if it's DM
// Ignore groups and other multiple participants chats
const chatId = event.payload.from;
if (!isJidCusFormat(chatId) && !isLidUser(chatId)) {
return false;
}
const payload = event.payload;
// Only READ and PLAYED
const read = payload.ack === WAMessageAck.READ;
const played = payload.ack == WAMessageAck.PLAYED;
if (!read && !played) {
return false;
}
// Only process when OUR message (fromMe: true) was read by recipient
if (!payload.fromMe) {
return false;
}
// Mark ChatWoot conversation as read when recipient reads our message
return true;
}
@@ -11,7 +11,8 @@ import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/uti
import { WAMessage } from '@waha/structures/responses.dto';
import type { proto } from '@adiwajshing/baileys';
import * as lodash from 'lodash';
import { fetchBuffer } from '@waha/core/abc/session.abc';
import { fetchBuffer } from '@waha/utils/fetch';
export class FacebookAdMessage implements MessageToChatWootConverter {
constructor(
@@ -116,8 +116,7 @@ describe('PixMessage - pt-BR', () => {
****Tipo:**** PHONE
****Referência:**** 4TB11111111
💳 ****PIX Copia e Cola enviado****
`;
💳 ****PIX Copia e Cola enviado****`;
expect(protoMessage).not.toBeNull();
expect(result).not.toBeNull();
@@ -237,8 +236,7 @@ describe('PixMessage - pt-BR', () => {
****Valor:**** R$\u00A025,12
****Referência:**** 4TB11111111
💳 ****PIX Copia e Cola enviado****
`;
💳 ****PIX Copia e Cola enviado****`;
expect(protoMessage).not.toBeNull();
expect(result).not.toBeNull();
@@ -14,4 +14,43 @@ describe('WhatsappToMarkdown', () => {
const expected = `**bold** | *italic* | ~~strike~~ | \`one line code\` | \`\`\`\nmultiple lines\`\`\` `;
expect(WhatsappToMarkdown(input)).toBe(expected);
});
// Tests for URL protection
it('preserves URLs with single underscore', () => {
const input = 'https://example.com/page_name';
expect(WhatsappToMarkdown(input)).toBe('https://example.com/page_name');
});
it('preserves URLs with double underscores', () => {
const input = 'https://en.wikipedia.org/wiki/Sarah_Jessica_Parker';
expect(WhatsappToMarkdown(input)).toBe(
'https://en.wikipedia.org/wiki/Sarah_Jessica_Parker',
);
});
it('preserves URLs with multiple underscores', () => {
const input = 'https://example.com/a_b_c_test';
expect(WhatsappToMarkdown(input)).toBe('https://example.com/a_b_c_test');
});
it('handles mixed text with URLs and formatting', () => {
const input = 'Veja *isso* em https://example.com/test_link e _aquilo_';
const expected =
'Veja **isso** em https://example.com/test_link e *aquilo*';
expect(WhatsappToMarkdown(input)).toBe(expected);
});
it('handles multiple URLs in same text', () => {
const input =
'Links: https://example.com/page_1 e https://example.com/page_2';
expect(WhatsappToMarkdown(input)).toBe(
'Links: https://example.com/page_1 e https://example.com/page_2',
);
});
it('handles URLs with underscores and regular italic formatting', () => {
const input = 'URL: https://example.com/test_page e texto _em itálico_';
const expected = 'URL: https://example.com/test_page e texto *em itálico*';
expect(WhatsappToMarkdown(input)).toBe(expected);
});
});
@@ -1,3 +1,5 @@
import { UrlProtection } from '@waha/apps/chatwoot/messages/to/markdown';
export function WhatsappToMarkdown(text: string): string {
if (!text) {
return text;
@@ -5,13 +7,22 @@ export function WhatsappToMarkdown(text: string): string {
if (text == '') {
return '';
}
return (
text
// Bold: *bold* → **bold**
.replace(/\*(.*?)\*/g, '**$1**')
// Strikethrough: ~strike~ → ~~strike~~
.replace(/~(.*?)~/g, '~~$1~~')
// Italic: _italic_ → *italic*
.replace(/_(.*?)_/g, '*$1*')
);
// Extract and protect URLs before formatting transformations
const protection = new UrlProtection();
const urls = protection.protect(text);
text = urls.text;
// Apply markdown transformations to "clean" text
let result = text
// Bold: *bold* → **bold**
.replace(/\*(.*?)\*/g, '**$1**')
// Strikethrough: ~strike~ → ~~strike~~
.replace(/~(.*?)~/g, '~~$1~~')
// Italic: _italic_ → *italic*
.replace(/_(.*?)_/g, '*$1*');
// Restore original URLs after all transformations
result = urls.restore(result);
return result;
}
+36
View File
@@ -0,0 +1,36 @@
const DEFAULT_PLACEHOLDER_PREFIX = 'URLPLACEHOLDER';
const URL_REGEX = /https?:\/\/[^\s]+/g;
export class UrlProtection {
/** Replaces URLs with placeholders so downstream formatters don't touch them. */
constructor(
private readonly placeholder: string = DEFAULT_PLACEHOLDER_PREFIX,
) {}
protect(text: string): ProtectedUrlText {
const urls: string[] = [];
const result = text.replace(URL_REGEX, (url) => {
const index = urls.length;
urls.push(url);
return `${this.placeholder}${index}`;
});
return new ProtectedUrlText(result, urls, this.placeholder);
}
}
export class ProtectedUrlText {
/** Carries protected text and restores URLs back into place. */
constructor(
public text: string,
private readonly urls: string[],
private readonly placeholder: string,
) {}
restore(text: string = this.text): string {
return this.urls.reduce((acc, url, index) => {
const placeholder = `${this.placeholder}${index}`;
return acc.replace(placeholder, url);
}, text);
}
}
@@ -14,4 +14,41 @@ describe('MarkdownToWhatsApp', () => {
const expected = `Here is a code block:\n\`\`\`some code\`\`\`\n*bold* _italic_ ~strike~ example (http://example.com)\n* item`;
expect(MarkdownToWhatsApp(input)).toBe(expected);
});
// Tests for URL protection
it('preserves URLs with multiple underscores', () => {
const input = 'https://example.com/page__name__test';
expect(MarkdownToWhatsApp(input)).toBe(
'https://example.com/page__name__test',
);
});
it('preserves URLs with double underscores', () => {
const input = 'https://en.wikipedia.org/wiki/Sarah_Jessica_Parker';
expect(MarkdownToWhatsApp(input)).toBe(
'https://en.wikipedia.org/wiki/Sarah_Jessica_Parker',
);
});
it('preserves URLs inside markdown links', () => {
const input =
'Veja [perfil](https://en.wikipedia.org/wiki/Sarah_Jessica_Parker)';
const expected =
'Veja perfil (https://en.wikipedia.org/wiki/Sarah_Jessica_Parker)';
expect(MarkdownToWhatsApp(input)).toBe(expected);
});
it('handles mixed text with URLs and formatting', () => {
const input = 'Veja **isso** em https://example.com/test_link e _aquilo_';
const expected = 'Veja *isso* em https://example.com/test_link e _aquilo_';
expect(MarkdownToWhatsApp(input)).toBe(expected);
});
it('handles multiple URLs in the same text', () => {
const input =
'Links: https://example.com/page_1 e https://example.com/page_2';
expect(MarkdownToWhatsApp(input)).toBe(
'Links: https://example.com/page_1 e https://example.com/page_2',
);
});
});
+2 -17
View File
@@ -5,8 +5,8 @@ import {
import { MessagesForRead } from '@waha/core/utils/convertors';
import {
IgnoreJidConfig,
isJidNewsletter,
isJidBroadcast,
isJidNewsletter,
JidFilter,
} from '@waha/core/utils/jids';
import {
@@ -43,7 +43,6 @@ import { complete } from '@waha/utils/reactive/complete';
import { SwitchObservable } from '@waha/utils/reactive/SwitchObservable';
import axios from 'axios';
import axiosRetry from 'axios-retry';
import { Agent as HttpsAgent } from 'https';
import * as fs from 'fs';
import * as lodash from 'lodash';
import * as NodeCache from 'node-cache';
@@ -129,6 +128,7 @@ import {
import { IMediaManager } from '../media/IMediaManager';
import { QR } from '../QR';
import { DataStore } from './DataStore';
import { fetchBuffer } from '@waha/utils/fetch';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const qrcode = require('qrcode-terminal');
@@ -1081,21 +1081,6 @@ export abstract class WhatsappSession {
}
}
const InsecureHttpsAgent = new HttpsAgent({
rejectUnauthorized: false,
});
export async function fetchBuffer(url: string): Promise<Buffer> {
return axios
.get(url, {
responseType: 'arraybuffer',
httpsAgent: InsecureHttpsAgent,
})
.then((res) => {
return Buffer.from(res.data);
});
}
export function getGroupInviteLink(code: string) {
if (code.startsWith('https://')) {
return code;
+19 -20
View File
@@ -1,4 +1,5 @@
import type { proto, WAMessageKey } from '@adiwajshing/baileys';
import { normalizeMessageContent, WAMessageKey } from '@adiwajshing/baileys';
import { proto } from '@adiwajshing/baileys';
import * as grpc from '@grpc/grpc-js';
import { connectivityState } from '@grpc/grpc-js';
import { UnprocessableEntityException } from '@nestjs/common';
@@ -166,6 +167,7 @@ import {
isLabelUpsertEvent,
} from './labels.gows';
import esm from '@waha/vendor/esm';
import { IsEditedMessage } from '@waha/core/utils/pwa';
enum WhatsMeowEvent {
CONNECTED = 'gows.ConnectedEventData',
@@ -491,19 +493,15 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
// Handle edited messages
const messagesEdited$ = messages$.pipe(
filter((msg) => {
return (
msg?.Message?.protocolMessage?.type === 14 &&
msg?.Message?.protocolMessage?.editedMessage !== undefined
);
}),
filter((message) => IsEditedMessage(message.Message)),
mergeMap(async (message): Promise<WAMessageEditedBody> => {
const waMessage = await this.toWAMessage(message);
const content = normalizeMessageContent(message.Message);
// Extract the body from editedMessage using extractBody function
const body =
extractBody(message.Message.protocolMessage.editedMessage) || '';
const body = extractBody(content.protocolMessage.editedMessage) || '';
// Extract the original message ID from protocolMessage.key
const editedMessageId = message.Message.protocolMessage.key?.ID;
// @ts-ignore
const editedMessageId = content.protocolMessage.key?.ID;
return {
...waMessage,
body: body,
@@ -1948,17 +1946,18 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
// if there is no text or media message
if (!message) return;
if (!message.Message) return;
// Ignore reactions, we have a dedicated handler for that
if (message.Message.reactionMessage) return;
// Ignore poll votes, we have a dedicated handler for that
if (message.Message.pollUpdateMessage) return;
// Ignore event response, we have a dedicated handler for that
if (message.Message.encEventResponseMessage) return;
// Ignore protocol messages
if (message.Message.protocolMessage) return;
const normalizedContent = esm.b.normalizeMessageContent(message.Message);
const contentType = esm.b.getContentType(normalizedContent);
const content = esm.b.normalizeMessageContent(message.Message);
// Ignore reactions, we have a dedicated handler for that
if (content.reactionMessage) return;
// Ignore poll votes, we have a dedicated handler for that
if (content.pollUpdateMessage) return;
// Ignore event response, we have a dedicated handler for that
if (content.encEventResponseMessage) return;
// Ignore protocol messages
if (content.protocolMessage) return;
const contentType = esm.b.getContentType(content);
// Ignore device sent message
if (contentType == 'deviceSentMessage') {
return;
+9 -24
View File
@@ -199,6 +199,7 @@ import { NowebPersistentStore } from './store/NowebPersistentStore';
import { NowebStorageFactoryCore } from './store/NowebStorageFactoryCore';
import { ensureNumber, extractMediaContent } from './utils';
import { Agents } from '@waha/core/engines/noweb/types';
import { IsEditedMessage } from '@waha/core/utils/pwa';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const promiseRetry = require('promise-retry');
@@ -608,14 +609,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
// https://github.com/WhiskeySockets/Baileys/pull/855/
this.sock.ev.on('messages.upsert', ({ messages }) => {
for (const message of messages) {
const content = normalizeMessageContent(message.message);
const protocolMsg = content?.protocolMessage;
if (
protocolMsg !== null &&
protocolMsg !== undefined &&
protocolMsg.editedMessage &&
protocolMsg.key
) {
if (IsEditedMessage(message.message)) {
const content = normalizeMessageContent(message.message);
const protocolMsg = content?.protocolMessage;
this.sock?.ev.emit('messages.update', [
{
key: {
@@ -1897,22 +1893,15 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
this.events2.get(WAHAEvents.MESSAGE_REVOKED).switch(messagesRevoked$);
// Handle edited messages
// @ts-ignore
const messagesEdited$ = messagesUpsert$.pipe(
filter(
(message) =>
// @ts-ignore
message.message?.protocolMessage?.type ===
proto.Message.ProtocolMessage.Type.MESSAGE_EDIT &&
message.message?.protocolMessage?.editedMessage !== undefined,
),
filter((message) => IsEditedMessage(message.message)),
mergeMap(async (message): Promise<WAMessageEditedBody> => {
const waMessage = this.toWAMessage(message);
// Extract the body from editedMessage using extractBody function
const body =
extractBody(message.message.protocolMessage.editedMessage) || '';
const content = normalizeMessageContent(message.message);
const body = extractBody(content.protocolMessage.editedMessage) || '';
// Extract the original message ID from protocolMessage.key
const editedMessageId = message.message.protocolMessage.key?.id;
const editedMessageId = content.protocolMessage.key?.id;
return {
...waMessage,
body: body,
@@ -2208,11 +2197,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
)
return;
// Ignore edit, we have a dedicated event for that
if (
message.message?.protocolMessage?.type ===
proto.Message.ProtocolMessage.Type.MESSAGE_EDIT
)
return;
if (IsEditedMessage(message.message)) return;
if (
message.message?.protocolMessage?.type ===
proto.Message.ProtocolMessage.Type.EPHEMERAL_SYNC_RESPONSE
+6 -2
View File
@@ -1,4 +1,4 @@
import { jidNormalizedUser, WAMessageKey } from '@adiwajshing/baileys';
import type { WAMessageKey } from '@adiwajshing/baileys';
import { ensureSuffix } from '@waha/core/abc/session.abc';
export function isJidNewsletter(jid: string) {
@@ -36,6 +36,10 @@ export function isPnUser(jid: string) {
);
}
function normalizeJid(jid: string): string {
return jid.replace(/:\d+(?=@)/, '');
}
/**
* Convert from 11111111111@c.us to 11111111111@s.whatsapp.net
* @param chatId
@@ -135,7 +139,7 @@ export function toCusFormat(remoteJid) {
return remoteJid;
}
if (isLidUser(remoteJid)) {
return jidNormalizedUser(remoteJid);
return normalizeJid(remoteJid);
}
if (isJidNewsletter(remoteJid)) {
return remoteJid;
+22
View File
@@ -0,0 +1,22 @@
/**
* Proto WhatsApp Utils
*/
import { normalizeMessageContent, proto } from '@adiwajshing/baileys';
export function IsEditedMessage(message: proto.IMessage): boolean {
message = normalizeMessageContent(message);
if (!message) {
return false;
}
if (
message?.protocolMessage?.type !==
proto.Message.ProtocolMessage.Type.MESSAGE_EDIT
) {
return false;
}
if (message?.protocolMessage?.editedMessage == null) {
return false;
}
return true;
}
+7 -1
View File
@@ -150,7 +150,7 @@ export class SessionConfig {
})
@IsDynamicObject()
@IsOptional()
metadata?: Map<string, string>;
metadata?: Record<string, string>;
@ApiProperty({
example: null,
@@ -225,6 +225,12 @@ export class MeInfo {
})
lid?: string;
@ApiProperty({
example: '123123:123@s.whatsapp.net',
description: 'Your id with device number',
})
jid?: string;
pushName: string;
}
+1 -1
View File
@@ -153,7 +153,7 @@ export class WAHAWebhook<Payload = any> {
},
description: 'Metadata for the session.',
})
metadata?: Map<string, string>;
metadata?: Record<string, string>;
@ApiProperty({
example: WAHAEngine.WEBJS,
+17
View File
@@ -0,0 +1,17 @@
import axios from 'axios';
import { Agent as HttpsAgent } from 'https';
const InsecureHttpsAgent = new HttpsAgent({
rejectUnauthorized: false,
});
export async function fetchBuffer(url: string): Promise<Buffer> {
return axios
.get(url, {
responseType: 'arraybuffer',
httpsAgent: InsecureHttpsAgent,
})
.then((res) => {
return Buffer.from(res.data);
});
}
+1 -1
View File
@@ -30,7 +30,7 @@ export function getWAHAVersion(): WAHAVersion {
}
export const VERSION: WAHAEnvironment = {
version: '2025.10.3',
version: '2025.10.4',
engine: getEngineName(),
tier: getWAHAVersion(),
browser:
+1 -1
View File
@@ -6,7 +6,7 @@
},
"dashboard": {
"repo": "devlikeapro/dashboard",
"ref": "583ee12cd77a95ca48c25dece822e3e9b4e01a23"
"ref": "8345fb8594a9f8f51fc0a593d5cc1a2e4e786960"
}
}
}
+4 -4
View File
@@ -5,9 +5,9 @@ __metadata:
version: 6
cacheKey: 8
"@adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2025-10-10":
"@adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2025-10-19":
version: 7.0.0-rc.5
resolution: "@adiwajshing/baileys@https://github.com/devlikeapro/Baileys.git#commit=dc9b6b7589ba1d71ec85cff4a789b5709a60a714"
resolution: "@adiwajshing/baileys@https://github.com/devlikeapro/Baileys.git#commit=df07435e41a5b6062c51a4a74393558cecf8fc50"
dependencies:
"@cacheable/node-cache": ^1.4.0
"@hapi/boom": ^9.1.3
@@ -31,7 +31,7 @@ __metadata:
optional: true
link-preview-js:
optional: true
checksum: d79aca449353f170374e6647efe09a2e871219e0af176d62796f68860ad619ce715736380250521e304d3ca28c5af4baa397ee0a8875468f8c7ad026e95028b6
checksum: 065a0a3e086e307ef805321355914c97282d279e7cbe2350118562373c0d36aaf877b0789d7e339f45f9c24e31a425f699b2f905411500522d0dc95a31c8986a
languageName: node
linkType: hard
@@ -12511,7 +12511,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "waha@workspace:."
dependencies:
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2025-10-10"
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2025-10-19"
"@adiwajshing/keyed-db": ^0.2.4
"@aws-sdk/client-s3": ^3.633.0
"@aws-sdk/s3-request-presigner": ^3.633.0