Compare commits

...
45 Commits
Author SHA1 Message Date
devlikepro cf3385f624 Merge remote-tracking branch 'core/core' into core
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2026-01-15 12:19:57 +07:00
devlikepro 0346c5e7b9 [core] 2026.1.3 2026-01-15 12:15:15 +07:00
devlikepro 443652b0a7 [core] Apps - ignore no type in headers 2026-01-15 12:15:14 +07:00
devlikepro 1e73c3dc6a [core] WEBJS - disable-metrics 2026-01-15 12:15:13 +07:00
devlikepro afa9d4208f [core] ChatWoot - fix delete message on WEBJS
fix #1769
2026-01-15 12:15:13 +07:00
devlikepro 64eda25b5d [core] WEBJS - get chats use AUTHENTICATED
fix #1782
2026-01-15 12:15:12 +07:00
devlikepro cfc6c931a3 [core] Up WEBJS
fix #1810
2026-01-15 12:15:11 +07:00
devlikepro 35bc56c948 [core] Up GOWS - 1.0.30 2026-01-15 12:15:11 +07:00
devlikepro 371bde4c3e [core] Up NOWEB - proto 1031716404 2026-01-15 12:15:09 +07:00
devlikepro 9497e2900e [core] .env 2026-01-15 12:15:08 +07:00
devlikepro 51bbc9998a [core] GitHub Actions - retry 2026-01-15 12:15:07 +07:00
devlikepro 057f7bce56 [core] ChatWoot - fix sending attachments with no caption in a group chat
fix #1808
2026-01-15 12:15:06 +07:00
devlikepro 52412fb4cc [core] ChatWoot - remove @lid when mention it
fix #1803
2026-01-15 12:15:06 +07:00
devlikepro a57f3443de [core] Resolve media absolute path 2026-01-15 12:15:06 +07:00
devlikepro 9b2537e7c7 [core] Add retries to docker build 2026-01-15 12:15:05 +07:00
Cameron Jackson a378ad4c3d [core] Sidestep existing .env from being overwritten (#1770)
Following docker instructions blindly currently wipes .env if it's already configure
2026-01-12 16:54:56 +07:00
devlikepro 8f867e0b5d [core] 2026.1.2
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2026-01-12 16:52:10 +07:00
devlikepro d4f6ec18fd [core] GOWS - receive and send video note
fix #1798
2026-01-12 16:52:10 +07:00
devlikepro ac05dd83e9 [core] NOWEB - handle video note media 2026-01-12 16:50:35 +07:00
devlikepro 17c22f0cd4 [core] Up WEBSJ - fix send media to channels
fix #1775
2026-01-12 16:50:34 +07:00
devlikepro 971979c13f [core] Do not send double session.status
fix #1784
2026-01-12 16:50:34 +07:00
devlikepro 845e203637 [core] GOWS - stop sessions first, then grpc
fix #1784
2026-01-12 16:50:34 +07:00
devlikepro 7a2e1cd731 [core] GOWS - start session in the background 2026-01-12 16:50:34 +07:00
devlikepro 84ba011a4c [core] Up GOWS - add profiling 2026-01-12 16:50:33 +07:00
devlikepro 50399f1192 [core] Add associatedChildMessage to edited message
fix #1773
2026-01-12 16:50:33 +07:00
devlikepro a2517b248d [core] ChatWoot - handle not found app
fix #1290
fix #1790
2026-01-12 16:50:33 +07:00
devlikepro f02d95331a [core] ChatWoot - support @lid mention directly
fix #1767
closes devlikeapro/waha-plus#36
2026-01-12 16:50:32 +07:00
Berg Pinheiro cb1faff88c [core] ChatWoot - support @mention and @all in group messages
fix #1767
closes devlikeapro/waha-plus#36
2026-01-12 16:50:32 +07:00
devlikepro 3ff1c06eea [core] ChatWoot - add session info api in client 2026-01-12 16:50:32 +07:00
devlikepro c91d2c59eb [core] ChatWoot - add group participants api in client 2026-01-12 16:50:32 +07:00
devlikepro 4cf5a74d23 [core] Groups - allow @all be with other mentions - we resolve group participants anyway 2026-01-12 16:50:32 +07:00
devlikepro 13de42dbe0 [core] Up GOWS
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
fix #1749
2026-01-02 19:47:30 +07:00
devlikepro f99b86ed18 [core] WEBJS - can not read properties of null - client
fix #1763
closes #1764
2026-01-02 19:27:50 +07:00
devlikepro 07f6c78372 [core] 2026.1.1 2026-01-02 19:27:50 +07:00
devlikepro 83678553e3 [core] 2025.12.3
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-12-30 17:38:14 +07:00
devlikepro 9d1bbd780e [core] NOWEB - use device name if no browser 2025-12-30 17:38:14 +07:00
devlikepro 899f9f2e84 [core] WAHA_CLIENT_DEVICE_NAME in .env 2025-12-30 17:38:14 +07:00
devlikepro 1d1a083464 [core] GOWS, NOWEB - motion_video (WhatsApp Live Photo / Motion Photo) messages are not supported
fix #175
2025-12-30 17:38:14 +07:00
devlikepro fabc6753e6 [core] Up Dashboard 2025-12-30 17:38:14 +07:00
devlikepro a16f3b67fe [core] Add device and browser config - WEBJS, GOWS, NOWEB
fix #1109
2025-12-30 17:38:13 +07:00
devlikepro c0c3381a36 [core] WEBJS - Up user agent 2025-12-30 17:38:13 +07:00
devlikepro 212a27d9df [core] Up GOWS
fix #1746
2025-12-30 17:38:13 +07:00
devlikepro a18c1d82c1 [core] GOWS - use a new grpc client for each session 2025-12-30 17:38:13 +07:00
devlikepro 67349e4509 [core] WEBJS - fix empty /me when WORKING session
fix #1735

[core] WEBJS
2025-12-30 17:38:13 +07:00
devlikepro 293267536b [core] Add platform to /version 2025-12-30 17:38:12 +07:00
35 changed files with 615 additions and 76 deletions

No files matched your search

+3
View File
@@ -22,6 +22,9 @@ WHATSAPP_SWAGGER_ENABLED=True
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
WHATSAPP_DEFAULT_ENGINE=WEBJS
# "Firefox (YourApp)" in Linked Devices
# WAHA_CLIENT_DEVICE_NAME=YourApp
# Base URL for the API (used for webhooks, file URLs, etc.)
WAHA_BASE_URL=http://localhost:3000
# WAHA_PUBLIC_URL=http://localhost:3000
+45 -1
View File
@@ -100,7 +100,51 @@ jobs:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build
- name: Build (attempt 1)
id: build1
continue-on-error: 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: Wait to retry build
if: steps.build1.outcome == 'failure'
run: |
sleep 10
- name: Build (attempt 2)
id: build2
if: steps.build1.outcome == 'failure'
continue-on-error: 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: Wait to retry build
if: steps.build2.outcome == 'failure'
run: |
sleep 10
- name: Build (attempt 3)
if: steps.build2.outcome == 'failure'
uses: docker/build-push-action@v4
with:
context: .
+53 -1
View File
@@ -71,8 +71,60 @@ jobs:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build
- name: Build (attempt 1)
id: build1
if: ${{ steps.recent.outputs.has_recent == 'true' }}
continue-on-error: 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: Wait to retry build
if:
${{ steps.recent.outputs.has_recent == 'true' && steps.build1.outcome
== 'failure' }}
run: |
sleep 10
- name: Build (attempt 2)
id: build2
if:
${{ steps.recent.outputs.has_recent == 'true' && steps.build1.outcome
== 'failure' }}
continue-on-error: 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: Wait to retry build
if:
${{ steps.recent.outputs.has_recent == 'true' && steps.build2.outcome
== 'failure' }}
run: |
sleep 10
- name: Build (attempt 3)
if:
${{ steps.recent.outputs.has_recent == 'true' && steps.build2.outcome
== 'failure' }}
uses: docker/build-push-action@v4
with:
context: .
+2
View File
@@ -32,6 +32,8 @@ WAHA_PUBLIC_URL=http://localhost:3000
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
WHATSAPP_DEFAULT_ENGINE=GOWS
# WAHA_CLIENT_DEVICE_NAME=YourApp
# =================
# ===== APPS ======
# =================
+1 -1
View File
@@ -40,7 +40,7 @@ export class AxiosLogging {
// Log debug for 2xx/3xx responses
this.logger.debug(`${methodStr} ${status}:OK ${url}`);
if (type.startsWith('application/json')) {
if (type?.startsWith('application/json')) {
const data = JSON.stringify(response.data);
this.logger.trace(`${methodStr} ${status}:OK ${url} ${data}`);
} else {
+19
View File
@@ -146,6 +146,17 @@ export class WAHASelf {
.then((response) => response.data);
}
async getGroupParticipants(
session: string,
groupId: string,
opts?: RequestOptions,
) {
const url = `/api/${session}/groups/${groupId}/participants/v2`;
return await this.client
.get(url, { signal: opts?.signal })
.then((response) => response.data);
}
async getChannel(
session: string,
channelId: string,
@@ -371,6 +382,10 @@ export class WAHASessionAPI {
return this.api.getGroup(this.session, groupId, opts);
}
getGroupParticipants(groupId: string, opts?: RequestOptions): Promise<any> {
return this.api.getGroupParticipants(this.session, groupId, opts);
}
getChannel(channelId: string, opts?: RequestOptions): Promise<Channel> {
return this.api.getChannel(this.session, channelId, opts);
}
@@ -459,4 +474,8 @@ export class WAHASessionAPI {
findLIDByPN(pn: string, opts?: RequestOptions) {
return this.api.findLIDByPN(this.session, pn, opts);
}
getSessionInfo(opts?: RequestOptions) {
return this.api.get(this.session, opts);
}
}
+10 -1
View File
@@ -8,6 +8,7 @@ import { InboxData } from '@waha/apps/chatwoot/consumers/types';
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
import {
ChatIDNotFoundForContactError,
ChatWootAppNotFoundError,
PhoneNumberNotFoundInWhatsAppError,
} from '@waha/apps/chatwoot/errors';
import { WAHASessionAPI } from '@waha/apps/app_sdk/waha/WAHASelf';
@@ -43,7 +44,11 @@ export abstract class ChatWootInboxMessageConsumer extends AppConsumer {
const knex = this.manager.store.getWAHADatabase();
this.appRepository = new AppRepository(knex);
const logger = new JobLoggerWrapper(job, this.logger);
const app = await this.appRepository.getById(appId);
const app = await this.appRepository.findEnabledAppById(appId);
if (!app) {
logger.warn(`Chatwoot app not found or disabled: ${appId}`);
throw new ChatWootAppNotFoundError(appId);
}
return new DIContainer(app.pk, app.config, logger, knex);
}
@@ -86,6 +91,10 @@ export abstract class ChatWootInboxMessageConsumer extends AppConsumer {
await this.ReportErrorRecovered(job, body);
return result;
} catch (err) {
if (err instanceof ChatWootAppNotFoundError) {
this.logger.warn(err.message);
throw err;
}
await this.ReportErrorForMessage(job, err, body);
throw err;
}
@@ -16,6 +16,7 @@ import {
MessageMappingService,
} from '@waha/apps/chatwoot/storage';
import { MarkdownToWhatsApp } from '@waha/apps/chatwoot/messages/to/whatsapp/markdown';
import { parseMentionsFromText } from '@waha/apps/chatwoot/messages/to/whatsapp/mentions';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import {
@@ -36,6 +37,7 @@ import {
LinkPreview,
} from '@waha/apps/chatwoot/dto/config.dto';
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
import { isJidGroup } from '@waha/core/utils/jids';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mime = require('mime-types');
@@ -90,7 +92,16 @@ export class MessageHandler {
return;
}
const content = MarkdownToWhatsApp(message.content);
let content: string = message.content;
let mentions: null | string[] = null;
if (isJidGroup(chatId)) {
// Parse any mention in the text and get a new clean text if required
const parsed = parseMentionsFromText(message.content);
content = parsed.text;
mentions = parsed.mentions;
}
content = MarkdownToWhatsApp(content);
const results = [];
let part = 0; // Start from 0 and increment before each send/check
const replyTo = await this.getReplyTo(message).catch((err) => {
@@ -116,7 +127,7 @@ export class MessageHandler {
content: content,
chatwoot: body,
});
const msg = await this.sendTextMessage(chatId, text, replyTo);
const msg = await this.sendTextMessage(chatId, text, replyTo, mentions);
results.push(msg);
await this.saveMapping(message, msg, part);
this.logger.info(`Text message sent: ${msg.id}`);
@@ -139,7 +150,7 @@ export class MessageHandler {
chatwoot: body,
singleAttachment: attachments.length == 1,
});
const msg = await this.sendFile(chatId, caption, file, replyTo);
const msg = await this.sendFile(chatId, caption, replyTo, mentions, file);
this.logger.info(
`File message sent: ${msg.id} - ${file.data_url} - ${file.file_type}`,
);
@@ -196,6 +207,7 @@ export class MessageHandler {
chatId: string,
content: string,
replyTo: string,
mentions: string[] | null,
) {
const request: MessageTextRequest = {
session: '',
@@ -206,6 +218,7 @@ export class MessageHandler {
this.config.linkPreview,
),
linkPreviewHighQuality: this.config.linkPreview == LinkPreview.HQ,
mentions: mentions,
};
const session = this.session;
await session.readMessages(chatId);
@@ -219,8 +232,9 @@ export class MessageHandler {
private async sendFile(
chatId: string,
content: string,
file: MessageAttachment,
replyTo: string,
mentions: string[] | null,
file: MessageAttachment,
) {
const url = file.data_url;
let filename = url.split('/').pop();
@@ -245,6 +259,7 @@ export class MessageHandler {
url: url,
mimetype: mimetype,
},
mentions: mentions,
};
return session.sendImage(imageRequest);
case 'video':
@@ -262,6 +277,7 @@ export class MessageHandler {
filename: filename,
},
convert: true,
mentions: mentions,
};
return session.sendVideo(videoRequest);
case 'audio':
@@ -288,6 +304,7 @@ export class MessageHandler {
url: url,
mimetype: mimetype,
},
mentions: mentions,
};
return session.sendFile(fileRequest);
}
+10 -1
View File
@@ -11,6 +11,7 @@ import { PinoLogger } from 'nestjs-pino';
import { AppRepository } from '../../storage';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { ChatWootAppNotFoundError } from '@waha/apps/chatwoot/errors';
/**
* Base class for ChatWoot scheduled consumers
@@ -35,7 +36,11 @@ export abstract class ChatWootScheduledConsumer extends AppConsumer {
const knex = this.manager.store.getWAHADatabase();
this.appRepository = new AppRepository(knex);
const logger = new JobLoggerWrapper(job, this.logger);
const app = await this.appRepository.getById(appId);
const app = await this.appRepository.findEnabledAppById(appId);
if (!app) {
logger.warn(`Chatwoot app not found or disabled: ${appId}`);
throw new ChatWootAppNotFoundError(appId);
}
return new DIContainer(app.pk, app.config, logger, knex);
}
@@ -62,6 +67,10 @@ export abstract class ChatWootScheduledConsumer extends AppConsumer {
await this.ReportErrorRecovered(job);
return result;
} catch (err) {
if (err instanceof ChatWootAppNotFoundError) {
this.logger.warn(err.message);
throw err;
}
await this.ReportErrorForJob(job, err);
throw err;
}
+10 -1
View File
@@ -13,6 +13,7 @@ import { AppRepository } from '../../storage';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { SignalRace } from '@waha/utils/abortable';
import { IsCommandsChat } from '@waha/apps/chatwoot/client/ids';
import { ChatWootAppNotFoundError } from '@waha/apps/chatwoot/errors';
/**
* Base class for ChatWoot background task consumers
@@ -36,7 +37,11 @@ export abstract class ChatWootTaskConsumer extends AppConsumer {
const knex = this.manager.store.getWAHADatabase();
this.appRepository = new AppRepository(knex);
const logger = new JobLoggerWrapper(job, this.logger);
const app = await this.appRepository.getById(appId);
const app = await this.appRepository.findEnabledAppById(appId);
if (!app) {
logger.warn(`Chatwoot app not found or disabled: ${appId}`);
throw new ChatWootAppNotFoundError(appId);
}
return new DIContainer(app.pk, app.config, logger, knex);
}
@@ -68,6 +73,10 @@ export abstract class ChatWootTaskConsumer extends AppConsumer {
await this.ReportErrorRecovered(job);
return result;
} catch (err) {
if (err instanceof ChatWootAppNotFoundError) {
this.logger.warn(err.message);
throw err;
}
this.ReportErrorForJob(job, err).catch((exc) =>
this.logger.error(`Failed to report error for job ${job.id}: ${exc}`),
);
+10 -1
View File
@@ -12,6 +12,7 @@ import { WhatsAppChatIdKey } from '@waha/apps/chatwoot/consumers/mutex';
import { EventData } from '@waha/apps/chatwoot/consumers/types';
import { WhatsAppContactInfo } from '@waha/apps/chatwoot/contacts/WhatsAppContactInfo';
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
import { ChatWootAppNotFoundError } from '@waha/apps/chatwoot/errors';
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
import { WAHASelf, WAHASessionAPI } from '@waha/apps/app_sdk/waha/WAHASelf';
import {
@@ -83,6 +84,10 @@ export abstract class ChatWootWAHABaseConsumer extends AppConsumer {
await this.ReportErrorRecovered(errorReportInfo, job);
return result;
} catch (err) {
if (err instanceof ChatWootAppNotFoundError) {
this.logger.warn(err.message);
throw err;
}
await this.ReportErrorForMessage(errorReportInfo, job, err);
throw err;
}
@@ -95,7 +100,11 @@ export abstract class ChatWootWAHABaseConsumer extends AppConsumer {
const knex = this.manager.store.getWAHADatabase();
this.appRepository = new AppRepository(knex);
const logger = new JobLoggerWrapper(job, this.logger);
const app = await this.appRepository.getById(appId);
const app = await this.appRepository.findEnabledAppById(appId);
if (!app) {
logger.warn(`Chatwoot app not found or disabled: ${appId}`);
throw new ChatWootAppNotFoundError(appId);
}
return new DIContainer(app.pk, app.config, logger, knex);
}
@@ -45,6 +45,12 @@ export class ChatWootErrorReporter {
// There's retries more left - ignore it for now
return;
}
if (!conversation) {
this.logger.error(
'Chatwoot error report skipped: conversation not ready',
);
return;
}
const attempts = {
current: this.job.attemptsMade + 1,
max: this.job.opts?.attempts || 1,
@@ -87,6 +93,12 @@ export class ChatWootErrorReporter {
// https://github.com/devlikeapro/waha/issues/1395
return null;
if (!conversation) {
this.logger.warn(
'Chatwoot success report skipped: conversation not ready',
);
return;
}
const template = this.l.key(TKey.JOB_REPORT_SUCCEEDED);
const attempts = {
current: this.job.attemptsMade + 1,
+6
View File
@@ -15,3 +15,9 @@ export class UnknownJIDFormat extends Error {
super(`WhatsApp Chat ID is not recognized: ${jid}`);
}
}
export class ChatWootAppNotFoundError extends Error {
constructor(public appId: string) {
super(`Chatwoot app not found or disabled: ${appId}`);
}
}
@@ -0,0 +1,130 @@
import { parseMentionsFromText } from './mentions';
describe('parseMentionsFromText', () => {
it('returns null mentions when none exist', () => {
const input = 'Hello there';
expect(parseMentionsFromText(input)).toEqual({
text: 'Hello there',
mentions: null,
});
});
it('handles empty content safely', () => {
expect(parseMentionsFromText(null)).toEqual({
text: null,
mentions: null,
});
});
it('parses @all and removes it from the text', () => {
const input = 'Hello @all';
expect(parseMentionsFromText(input)).toEqual({
text: 'Hello',
mentions: ['all'],
});
});
it('parses @ALL case-insensitively', () => {
const input = 'Hello @ALL';
expect(parseMentionsFromText(input)).toEqual({
text: 'Hello',
mentions: ['all'],
});
});
it('uses a fallback text when only @all is present', () => {
const input = '@all';
expect(parseMentionsFromText(input)).toEqual({
text: ' ',
mentions: ['all'],
});
});
it('parses phone number mentions with a suffix', () => {
const input = 'Ping @558591203123';
expect(parseMentionsFromText(input)).toEqual({
text: 'Ping @558591203123',
mentions: ['558591203123@c.us'],
});
});
it('deduplicates phone number mentions', () => {
const input = 'Hi @1111111 and @1111111';
expect(parseMentionsFromText(input)).toEqual({
text: 'Hi @1111111 and @1111111',
mentions: ['1111111@c.us'],
});
});
it('handles @all alongside phone mentions', () => {
const input = '@all Hi @558591203123';
expect(parseMentionsFromText(input)).toEqual({
text: 'Hi @558591203123',
mentions: ['all', '558591203123@c.us'],
});
});
it('ignores quoted @all text', () => {
const input = "You can send '@all' to chat";
expect(parseMentionsFromText(input)).toEqual({
text: "You can send '@all' to chat",
mentions: null,
});
});
it('ignores email-like mentions', () => {
const input = 'Reach us at email@12341domain.com';
expect(parseMentionsFromText(input)).toEqual({
text: 'Reach us at email@12341domain.com',
mentions: null,
});
});
it('ignores @all when followed by letters', () => {
const input = 'Please ping @allx';
expect(parseMentionsFromText(input)).toEqual({
text: 'Please ping @allx',
mentions: null,
});
});
it('ignores phone mentions followed by letters', () => {
const input = 'Call @1234567abc';
expect(parseMentionsFromText(input)).toEqual({
text: 'Call @1234567abc',
mentions: null,
});
});
it('ignores @all inside email-like text', () => {
const input = 'Contact email@alldomain.com for details';
expect(parseMentionsFromText(input)).toEqual({
text: 'Contact email@alldomain.com for details',
mentions: null,
});
});
it('accepts phone mentions with trailing punctuation', () => {
const input = 'Ping @1231234, then @5555555.';
expect(parseMentionsFromText(input)).toEqual({
text: 'Ping @1231234, then @5555555.',
mentions: ['1231234@c.us', '5555555@c.us'],
});
});
it('parses @lid mentions without c.us suffix', () => {
const input = 'Notify @123123@lid now';
expect(parseMentionsFromText(input)).toEqual({
text: 'Notify @123123 now',
mentions: ['123123@lid'],
});
});
it('replaces @lid suffix with plain ids', () => {
const input = '@123456@lid please ping @654321@lid.';
expect(parseMentionsFromText(input)).toEqual({
text: '@123456 please ping @654321.',
mentions: ['123456@lid', '654321@lid'],
});
});
});
@@ -0,0 +1,91 @@
import { ensureSuffix } from '@waha/core/abc/session.abc';
import { isJidGroup } from '@waha/core/utils/jids';
export interface ParsedMentions {
text: string;
mentions?: string[];
}
/**
* Parse mentions from text message
* Supports:
* - @all - mentions all group participants
* - @558591203123 - mentions specific phone number
*
* @param text - The message text
* @returns Parsed mentions with cleaned text
*/
export function parseMentionsFromText(
text: string | null | undefined,
): ParsedMentions {
if (!text) {
return {
text: text,
mentions: null,
};
}
const mentions: string[] = [];
let content = text;
// Regex to match @all (case-insensitive, word boundary)
// Use separate regex instances to avoid lastIndex issues with global flag
const allRegexTest = /(^|\s)@all\b/gi;
if (allRegexTest.test(text)) {
mentions.push('all');
// Remove @all from text - WAHA endpoint doesn't display it, only mentions all participants
// Create a new regex instance for replace to avoid lastIndex issues
const allRegexReplace = /(^|\s)@all\b/gi;
content = content.replace(allRegexReplace, '$1').trim();
}
const lidRegex = /(^|\s)@(\d{6,15})@lid\b/g;
const lidMatches = text.matchAll(lidRegex);
for (const match of lidMatches) {
const lidNumber = match[2];
const formattedLid = `${lidNumber}@lid`;
if (!mentions.includes(formattedLid)) {
mentions.push(formattedLid);
}
}
if (mentions.length > 0) {
const lidRegexReplace = /(^|\s)@(\d{6,15})@lid\b/g;
content = content.replace(lidRegexReplace, '$1@$2').trim();
}
// Regex to match @ followed by phone number
// Matches: @558591203123, @5585991203123, etc.
// Phone numbers: 7-15 digits (international format)
const phoneRegex = /(^|\s)@(\d{7,15})(?!@lid)\b/g;
const phoneMatches = text.matchAll(phoneRegex);
for (const match of phoneMatches) {
const phoneNumber = match[2];
// Format phone number to @c.us format
const formattedJid = ensureSuffix(phoneNumber);
if (!mentions.includes(formattedJid)) {
mentions.push(formattedJid);
}
}
if (mentions.length == 0) {
// Nothing found
return {
text: text,
mentions: null,
};
}
// Ensure text is not empty when there are mentions
// If text becomes empty after removing mentions, use a default message
content = content.trim();
if (content === '') {
// When only mentions are present, use a minimal text to satisfy DTO requirements
// The mentions array will handle the actual mentions
content = ' ';
}
return {
text: content,
mentions: mentions,
};
}
+1 -1
View File
@@ -124,7 +124,7 @@ class WEBJSHelper implements IEngineHelper {
return {
timestamp: new Date(message.timestamp * 1000),
from_me: message.fromMe,
chat_id: message.from,
chat_id: message.id.remote,
message_id: message.id.id,
participant: message.author || null,
};
+3 -2
View File
@@ -50,7 +50,6 @@ import * as lodash from 'lodash';
import * as NodeCache from 'node-cache';
import { Logger } from 'pino';
import {
BehaviorSubject,
catchError,
delay,
filter,
@@ -228,7 +227,8 @@ export abstract class WhatsappSession {
engineConfig,
ignore,
}: SessionParams) {
this.status$ = new BehaviorSubject(WAHASessionStatus.STOPPED);
this._status = WAHASessionStatus.STOPPED;
this.status$ = new Subject<WAHASessionStatus>();
this.name = name;
this.proxyConfig = proxyConfig;
@@ -381,6 +381,7 @@ export abstract class WhatsappSession {
'--disable-default-apps',
'--disable-dev-shm-usage',
'--disable-extensions',
'--disable-metrics',
// '--disable-features=site-per-process', // COMMENTED to test WEBJS stability
'--disable-gpu', // COMMENTED to test WEBJS stability
'--disable-offer-store-unmasked-wallet-cards',
+1 -1
View File
@@ -142,7 +142,7 @@ export function ReportGeneratedValue() {
'Save these values to your environment (.env or WAHA_*) to reuse them; new keys are generated on every start otherwise.',
);
console.warn('');
console.warn("cat <<'EOF' > .env");
console.warn("cat <<'EOF' >> .env");
console.warn('');
for (const key of values) {
console.warn(`${key.param}=${key.value}`);
@@ -22,7 +22,7 @@ export class GowsEventStreamObservable extends Observable<EnginePayload> {
},
) {
super((subscriber) => {
logger.debug('Creating grpc stream...');
logger.debug('Creating grpc client and stream...');
logger.setBindings({ id: rand() });
const { client, stream } = factory();
this._client = client;
@@ -41,6 +41,13 @@ export class GowsEventStreamObservable extends Observable<EnginePayload> {
logger.warn({ err }, 'Failed to cancel gRPC stream');
}
logger.debug({ reason }, 'Closing gRPC client...');
try {
client.close();
} catch (err) {
logger.warn({ err }, 'Failed to close gRPC client');
}
await sleep(this.CLIENT_CLOSE_TIMEOUT);
};
+8 -24
View File
@@ -1,25 +1,15 @@
import * as grpc from '@grpc/grpc-js';
import { messages } from '@waha/core/engines/gows/grpc/gows';
import { Pool, SizedPool } from '@waha/core/engines/gows/pools';
let CLIENTS: Pool<messages.MessageServiceClient> | null = null;
let STREAM_CLIENTS: Pool<messages.EventStreamClient> | null = null;
export const GetMessageServiceClient = (
session: string,
address: string,
credentials: grpc.ChannelCredentials,
): messages.MessageServiceClient => {
if (!CLIENTS) {
const factory = () => {
return new messages.MessageServiceClient(address, credentials, {
'grpc.max_send_message_length': 128 * 1024 * 1024,
'grpc.max_receive_message_length': 128 * 1024 * 1024,
});
};
CLIENTS = new SizedPool(16, factory);
}
return CLIENTS.get(session);
return new messages.MessageServiceClient(address, credentials, {
'grpc.max_send_message_length': 128 * 1024 * 1024,
'grpc.max_receive_message_length': 128 * 1024 * 1024,
});
};
export const GetEventStreamClient = (
@@ -27,14 +17,8 @@ export const GetEventStreamClient = (
address: string,
credentials: grpc.ChannelCredentials,
): messages.EventStreamClient => {
if (!STREAM_CLIENTS) {
const factory = () => {
return new messages.EventStreamClient(address, credentials, {
'grpc.max_send_message_length': 128 * 1024 * 1024,
'grpc.max_receive_message_length': 128 * 1024 * 1024,
});
};
STREAM_CLIENTS = new SizedPool(16, factory);
}
return STREAM_CLIENTS.get(session);
return new messages.EventStreamClient(address, credentials, {
'grpc.max_send_message_length': 128 * 1024 * 1024,
'grpc.max_receive_message_length': 128 * 1024 * 1024,
});
};
+2 -1
View File
@@ -23,7 +23,8 @@ export namespace messages {
IMAGE = 0,
AUDIO = 1,
VIDEO = 2,
DOCUMENT = 3
DOCUMENT = 3,
PTV = 4
}
export enum Presence {
AVAILABLE = 0,
+2 -1
View File
@@ -21564,7 +21564,8 @@ proto.messages.MediaType = {
IMAGE: 0,
AUDIO: 1,
VIDEO: 2,
DOCUMENT: 3
DOCUMENT: 3,
PTV: 4
};
/**
+6 -8
View File
@@ -288,14 +288,12 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
grpc.credentials.createInsecure(),
);
try {
await promisify(this.client.StartSession)(request);
} catch (err) {
promisify(this.client.StartSession)(request).catch((err) => {
this.logger.error('Failed to start the client');
this.logger.error(err, err.stack);
this.status = WAHASessionStatus.FAILED;
throw err;
}
});
}
protected getProxyUrl(config: ProxyConfig): string {
@@ -766,14 +764,14 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
async stop(): Promise<void> {
this.cleanupPresenceTimeout();
if (this.client) {
const response = await promisify(this.client.StopSession)(this.session);
response.toObject();
}
this.status = WAHASessionStatus.STOPPED;
this.events?.stop();
this.stopEvents();
this.mediaManager.close();
if (this.client) {
await promisify(this.client.StopSession)(this.session);
this.client?.close();
}
}
public async requestCode(phoneNumber: string, method: string, params?: any) {
+41 -2
View File
@@ -1,4 +1,4 @@
import type { WABrowserDescription } from '@adiwajshing/baileys';
import { Browsers, WABrowserDescription } from '@adiwajshing/baileys';
import makeWASocket, {
Chat,
Contact,
@@ -205,6 +205,10 @@ import {
import { extractWALocation } from '@waha/core/engines/waproto/locaiton';
import { extractVCards } from '@waha/core/engines/waproto/vcards';
import { Activity } from '@waha/core/abc/activity';
import {
WAHA_CLIENT_BROWSER_NAME,
WAHA_CLIENT_DEVICE_NAME,
} from '@waha/core/env';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const promiseRetry = require('promise-retry');
@@ -314,8 +318,37 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
}
getSocketConfig(agents: Agents | undefined, state): Partial<SocketConfig> {
// Detect browser
let browser = ['Ubuntu', 'Chrome', '22.04.4'] as WABrowserDescription;
let deviceName =
this.sessionConfig?.client?.deviceName ?? WAHA_CLIENT_DEVICE_NAME;
let browserName =
this.sessionConfig?.client?.browserName ?? WAHA_CLIENT_BROWSER_NAME;
if (browserName && !deviceName) {
browser = Browsers.appropriate(browserName);
} else if (!browserName && deviceName) {
browser = [deviceName, 'Chrome', '22.04.4'];
} else if (browserName && deviceName) {
switch (deviceName) {
case 'Mac OS':
case 'MacOS':
case 'macos':
browser = Browsers.macOS(browserName);
break;
case 'ubuntu':
case 'Ubuntu':
browser = Browsers.ubuntu(browserName);
break;
case 'windows':
case 'Windows':
browser = Browsers.windows(browserName);
break;
default:
browser = [deviceName, browserName, '22.04.4'];
}
}
const fullSyncEnabled = this.sessionConfig?.noweb?.store?.fullSync || false;
const browser = ['Ubuntu', 'Chrome', '20.0.04'] as WABrowserDescription;
let markOnlineOnConnect = this.sessionConfig?.noweb?.markOnline;
if (markOnlineOnConnect == undefined) {
markOnlineOnConnect = true;
@@ -2877,6 +2910,12 @@ export function extractBody(message): string | null {
// @ts-ignore - AudioMessage doesn't have caption field
body = mediaContent?.caption;
}
if (!body && content.protocolMessage?.editedMessage) {
body = extractBody(content.protocolMessage.editedMessage);
}
if (!body && content.associatedChildMessage?.message) {
body = extractBody(content.associatedChildMessage.message);
}
// Response for buttons
if (!body) {
body = content.templateButtonReplyMessage?.selectedDisplayText;
+8 -1
View File
@@ -10,8 +10,15 @@ export function extractMediaContent(
content?.imageMessage ||
content?.videoMessage ||
content?.audioMessage ||
content?.ptvMessage ||
content?.stickerMessage;
return mediaContent;
if (mediaContent) {
return mediaContent;
}
if (content?.associatedChildMessage?.message) {
return extractMediaContent(content.associatedChildMessage.message);
}
return null;
}
interface Long {
+1 -1
View File
@@ -36,7 +36,7 @@ export class WebjsClientCore extends Client {
) {
super(options);
// Wait until it's READY and inject more utils
this.on(Events.READY, async () => {
this.on(Events.AUTHENTICATED, async () => {
await this.attachCustomEventListeners();
await this.injectWaha();
});
+47 -7
View File
@@ -121,11 +121,7 @@ import {
} from '@waha/structures/responses.dto';
import { BrowserTraceQuery } from '@waha/structures/server.debug.dto';
import { MeInfo } from '@waha/structures/sessions.dto';
import {
DeleteStatusRequest,
StatusRequest,
TextStatus,
} from '@waha/structures/status.dto';
import { DeleteStatusRequest, TextStatus } from '@waha/structures/status.dto';
import {
EnginePayload,
PollVote as WAHAPollVote,
@@ -185,6 +181,10 @@ import {
import { Activity } from '@waha/core/abc/activity';
import { CallData } from '@waha/structures/calls.dto';
import { Jid } from '@waha/core/engines/const';
import {
WAHA_CLIENT_BROWSER_NAME,
WAHA_CLIENT_DEVICE_NAME,
} from '@waha/core/env';
export interface WebJSConfig {
webVersion?: string;
@@ -246,6 +246,10 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
// add at the start
args.unshift(`--a-waha-timestamp=${new Date()}`);
args.unshift(`--a-waha-session=${this.name}`);
const deviceName =
this.sessionConfig?.client?.deviceName ?? WAHA_CLIENT_DEVICE_NAME;
const browserName =
this.sessionConfig?.client?.browserName ?? WAHA_CLIENT_BROWSER_NAME;
return {
puppeteer: {
protocolTimeout: 300_000,
@@ -255,7 +259,9 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
dumpio: this.isDebugEnabled(),
},
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
deviceName: deviceName,
browserName: browserName,
webVersion: webVersion,
webVersionCache: {
type: cacheType,
@@ -521,10 +527,29 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
});
this.whatsapp.on(Events.AUTHENTICATED, (args) => {
this.whatsapp.on(Events.AUTHENTICATED, async (args) => {
this.status = WAHASessionStatus.WORKING;
this.qr.save('');
this.logger.info({ args: args }, `Session has been authenticated!`);
// Try to get client info from puppeter if nothing set
// Fix https://github.com/devlikeapro/waha/issues/1735
await sleep(3_000);
if (!this.whatsapp.info) {
// try to load client info few times with a delay
for (let attempt = 0; attempt < 3; attempt++) {
await this.loadClientInfo().catch((error) =>
this.logger.error(
error,
`Failed to load client info, attempt ${attempt + 1}`,
),
);
if (this.whatsapp.info) {
break;
}
await sleep(3_000);
}
}
});
this.whatsapp.on(Events.AUTHENTICATION_FAILURE, (args) => {
@@ -585,6 +610,21 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
});
}
private async loadClientInfo() {
const data = await this.whatsapp.pupPage.evaluate(() => {
return {
// @ts-ignore
...window.Store.Conn.serialize(),
wid:
// @ts-ignore
window.Store.User.getMaybeMePnUser() ||
// @ts-ignore
window.Store.User.getMaybeMeLidUser(),
};
});
this.whatsapp.info = data as any;
}
/**
* START - Methods for API
*/
+8
View File
@@ -22,3 +22,11 @@ if (KNEX_SQLITE_CLIENT != 'sqlite3' && KNEX_SQLITE_CLIENT != 'better-sqlite3') {
KNEX_SQLITE_CLIENT = 'sqlite3';
}
export { KNEX_SQLITE_CLIENT };
//
// Client config
//
export const WAHA_CLIENT_DEVICE_NAME =
process.env.WAHA_CLIENT_DEVICE_NAME || null;
export const WAHA_CLIENT_BROWSER_NAME =
process.env.WAHA_CLIENT_BROWSER_NAME || null;
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { WhatsappConfigService } from '@waha/config.service';
import * as path from 'path';
@Injectable()
export class MediaLocalStorageConfig {
@@ -12,7 +13,14 @@ export class MediaLocalStorageConfig {
}
get filesFolder(): string {
return this.config.get('WHATSAPP_FILES_FOLDER', '/tmp/whatsapp-files');
const configured = this.config.get('WHATSAPP_FILES_FOLDER', '');
if (!configured) {
return '/tmp/whatsapp-files';
}
if (path.isAbsolute(configured)) {
return configured;
}
return path.resolve(process.cwd(), configured);
}
get filesLifetime(): number {
-5
View File
@@ -16,9 +16,4 @@ export function validateRequestMentions(request: {
`"mentions":["all"] can be used only in group chats, not in '${request.chatId}'`,
);
}
if (request.mentions.length > 1) {
throw new UnprocessableEntityException(
`"mentions":["all"] cannot be used with other mentions`,
);
}
}
+5
View File
@@ -20,4 +20,9 @@ export class WAHAEnvironment {
example: '/usr/path/to/bin/google-chrome',
})
browser: string;
@ApiProperty({
example: 'linux/x86',
})
platform: string;
}
+23
View File
@@ -155,6 +155,16 @@ export class IgnoreConfig {
broadcast?: boolean;
}
export class ClientSessionConfig {
@IsString()
@IsOptional()
deviceName?: string;
@IsString()
@IsOptional()
browserName?: string;
}
export class SessionConfig {
@ValidateNested({ each: true })
@Type(() => WebhookConfig)
@@ -204,6 +214,19 @@ export class SessionConfig {
@IsOptional()
ignore?: IgnoreConfig;
@ApiProperty({
description:
"How connected session renders in device - in format 'Browser (Device)' - Firefox (MacOS)",
example: {
browserName: 'Firefox',
deviceName: 'MacOS',
},
})
@ValidateNested()
@Type(() => ClientSessionConfig)
@IsOptional()
client?: ClientSessionConfig;
@ApiProperty({
example: {
store: {
+13 -3
View File
@@ -29,12 +29,22 @@ export function getWAHAVersion(): WAHAVersion {
return WAHAVersion.CORE;
}
function getBrowser() {
return getEngineName() === WAHAEngine.WEBJS
? getBrowserExecutablePath()
: null;
}
function getPlatform() {
return `${process.platform}/${process.arch}`;
}
export const VERSION: WAHAEnvironment = {
version: '2025.12.2',
version: '2026.1.3',
engine: getEngineName(),
tier: getWAHAVersion(),
browser:
getEngineName() === WAHAEngine.WEBJS ? getBrowserExecutablePath() : null,
browser: getBrowser(),
platform: getPlatform(),
};
export const IsChrome = VERSION.browser?.includes('chrome');
+2 -2
View File
@@ -2,11 +2,11 @@
"waha": {
"gows": {
"repo": "devlikeapro/gows",
"ref": "v1.0.26"
"ref": "v1.0.30"
},
"dashboard": {
"repo": "devlikeapro/dashboard",
"ref": "6d62eda142f95b5a6b40614f83d9c12e52957f85"
"ref": "6030cf5e08f2a6e6d09a7a70a1c4194baf0beb55"
}
}
}
+4 -4
View File
@@ -7,7 +7,7 @@ __metadata:
"@adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2025-12-17":
version: 7.0.0-rc.9
resolution: "@adiwajshing/baileys@https://github.com/devlikeapro/Baileys.git#commit=425b11f5b2d98c808210fab78afc6257f27f2fe5"
resolution: "@adiwajshing/baileys@https://github.com/devlikeapro/Baileys.git#commit=21289d1c97dd4b6b0c2585c0f77cecc486fb9f2f"
dependencies:
"@cacheable/node-cache": "npm:^1.4.0"
"@hapi/boom": "npm:^9.1.3"
@@ -31,7 +31,7 @@ __metadata:
optional: true
link-preview-js:
optional: true
checksum: 10/2b58dffc216fe6518e3f72532de60765e7d8c371cdc17221b79c98d74752a5b6c08f5ea2af4543b12bd6c55f1434d1b6b697e4b31f33fea50069b7b4b774feb1
checksum: 10/6d3cbdbbb490356b89d65310244a8120f650641efbfd43f6fea4c5296c17d75feaeb7d870ee2ba77015b97c94c7ffd1950d0e6466b5c0dbc5eae020e68cf85d5
languageName: node
linkType: hard
@@ -12756,7 +12756,7 @@ __metadata:
"whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2025-12-17":
version: 1.34.3
resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=8349fcd26fd2bbede0e6739b0da9b9ce97fe6c14"
resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=766bb3957a81338250ee830d34cfceec278e545e"
dependencies:
"@pedroslopez/moduleraid": "npm:^5.0.2"
archiver: "npm:^5.3.1"
@@ -12774,7 +12774,7 @@ __metadata:
optional: true
unzipper:
optional: true
checksum: 10/b53255dfd1c0336232f4f4683e67779bea2f002242084961ecec59844595414a80f7172859eb4a8983e9dd6c276de6a97bfdec5135c7d4ec6ee5ed638a6e8346
checksum: 10/cb54d6192bd5d24c5521d1d5599fc7da65e87c3dfc595cab708b7335e95842d31fd7c0d84a7c479202de55bda67c4dcaec802150568e5f35815b6b07fd669d57
languageName: node
linkType: hard