Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf3385f624 | ||
|
|
0346c5e7b9 | ||
|
|
443652b0a7 | ||
|
|
1e73c3dc6a | ||
|
|
afa9d4208f | ||
|
|
64eda25b5d | ||
|
|
cfc6c931a3 | ||
|
|
35bc56c948 | ||
|
|
371bde4c3e | ||
|
|
9497e2900e | ||
|
|
51bbc9998a | ||
|
|
057f7bce56 | ||
|
|
52412fb4cc | ||
|
|
a57f3443de | ||
|
|
9b2537e7c7 | ||
|
|
a378ad4c3d | ||
|
|
8f867e0b5d | ||
|
|
d4f6ec18fd | ||
|
|
ac05dd83e9 | ||
|
|
17c22f0cd4 | ||
|
|
971979c13f | ||
|
|
845e203637 | ||
|
|
7a2e1cd731 | ||
|
|
84ba011a4c | ||
|
|
50399f1192 | ||
|
|
a2517b248d | ||
|
|
f02d95331a | ||
|
|
cb1faff88c | ||
|
|
3ff1c06eea | ||
|
|
c91d2c59eb | ||
|
|
4cf5a74d23 |
No files matched your search
@@ -22,6 +22,7 @@ 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.)
|
||||
|
||||
@@ -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: .
|
||||
|
||||
@@ -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: .
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}`),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -23,7 +23,8 @@ export namespace messages {
|
||||
IMAGE = 0,
|
||||
AUDIO = 1,
|
||||
VIDEO = 2,
|
||||
DOCUMENT = 3
|
||||
DOCUMENT = 3,
|
||||
PTV = 4
|
||||
}
|
||||
export enum Presence {
|
||||
AVAILABLE = 0,
|
||||
|
||||
@@ -21564,7 +21564,8 @@ proto.messages.MediaType = {
|
||||
IMAGE: 0,
|
||||
AUDIO: 1,
|
||||
VIDEO: 2,
|
||||
DOCUMENT: 3
|
||||
DOCUMENT: 3,
|
||||
PTV: 4
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,15 +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.client?.close();
|
||||
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) {
|
||||
|
||||
@@ -2910,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;
|
||||
|
||||
@@ -10,6 +10,7 @@ export function extractMediaContent(
|
||||
content?.imageMessage ||
|
||||
content?.videoMessage ||
|
||||
content?.audioMessage ||
|
||||
content?.ptvMessage ||
|
||||
content?.stickerMessage;
|
||||
if (mediaContent) {
|
||||
return mediaContent;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -40,7 +40,7 @@ function getPlatform() {
|
||||
}
|
||||
|
||||
export const VERSION: WAHAEnvironment = {
|
||||
version: '2026.1.1',
|
||||
version: '2026.1.3',
|
||||
engine: getEngineName(),
|
||||
tier: getWAHAVersion(),
|
||||
browser: getBrowser(),
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"waha": {
|
||||
"gows": {
|
||||
"repo": "devlikeapro/gows",
|
||||
"ref": "v1.0.28"
|
||||
"ref": "v1.0.30"
|
||||
},
|
||||
"dashboard": {
|
||||
"repo": "devlikeapro/dashboard",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user