Compare commits

...
39 Commits
Author SHA1 Message Date
devlikepro 3b6fa3343d [core] 2025.6.4
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-06-21 17:34:41 +07:00
devlikepro 469cdc9b1d [core] no xvfb if not installed (noweb/gows image) 2025-06-21 17:34:41 +07:00
devlikepro 8a179e7194 [core] NOWEB - sleep for sqlite3 in batch 2025-06-21 17:34:41 +07:00
devlikepro c133795753 [core] NOWEB - log length, not json
fix devlikeapro/waha#1054
2025-06-21 17:34:41 +07:00
devlikepro 941ace7461 [core] 2025.6.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-06-16 13:05:20 +07:00
devlikepro 8589420f5d [core] do not stop ws socket in gateway 2025-06-16 13:05:20 +07:00
devlikepro ece24e182f [core] WEBJS - xvfb 2025-06-16 13:05:19 +07:00
devlikepro 38e5bb7bdf [core] NOWEB - Add timeout 5s for async lock 2025-06-16 13:05:19 +07:00
devlikepro d09e931827 [core] chrome 137.0.7151.103-1 2025-06-16 13:05:19 +07:00
devlikepro 3f3f1e1c20 [core] Inline WAMessage in message.edited 2025-06-16 13:05:18 +07:00
devlikepro 32f2468ba8 [core] Add WAHAWebhookMessageEdited to webhooks list 2025-06-16 13:05:17 +07:00
devlikepro 1d6540e0d9 [core] editedMessageId 2025-06-16 13:05:17 +07:00
devlikepro 31da4bfca1 [core] WEBJS - message.edited
fix #1041
2025-06-16 13:05:17 +07:00
devlikepro 41d49894e2 [core] WEBJS - message.revoked add revokedMessageId 2025-06-16 13:05:17 +07:00
devlikepro 6dcdd15b1c [core] message.revoked - add revokedMessageId - NOWEB, GOWS 2025-06-16 13:05:17 +07:00
devlikepro c2b275fd90 [core] NOWEB - message.edited 2025-06-16 13:05:16 +07:00
devlikepro e4838ef3ef [core] GOWS - message.edited
fix #916
2025-06-16 13:05:16 +07:00
devlikepro 55ca4660d7 [core] GOWS - message.revoked
fix #917
2025-06-16 13:05:15 +07:00
devlikepro c16c903ec4 [core] node 22.16 2025-06-16 13:05:15 +07:00
devlikepro 937cf38ff6 [core] Up libs 2025-06-16 13:05:15 +07:00
devlikepro 07f76e854d [core] Update axios 2025-06-16 13:05:15 +07:00
devlikepro cde1d084c6 [core] NOWEB fix issue with can not read property of null 2025-06-16 13:05:14 +07:00
devlikepro 9362c5d6ae [core] 2025.6.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
2025-06-11 15:41:56 +07:00
devlikepro 117a9108c0 [core] puppeter 24.10.0 2025-06-11 15:41:56 +07:00
devlikepro 5214e461f2 [core] chrome 137.0.7151.55 2025-06-11 15:41:56 +07:00
devlikepro 9bdb178b7d [core] Use github examples 2025-06-11 15:41:56 +07:00
devlikepro 9c526ffdba [core] Add converting voice and video files
fix #724
2025-06-11 15:41:56 +07:00
devlikepro f80a74edd1 [core] Add avi and mp3 in examples 2025-06-11 12:20:58 +07:00
devlikepro 7d8c6c0a62 [core] 2025.6.1
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-06-09 14:01:26 +07:00
devlikepro 17b26b869b [core] Add --pprof-host listen to 0.0.0.0 if debug mode enabled 2025-06-09 14:01:26 +07:00
devlikepro dfbe54e92f [core] Up GOWS
fix #919
2025-06-09 14:01:25 +07:00
devlikepro 2321de409b [core] Up WEBJS
fix #1023
fix #1024
fix #1025
fix #1026
fix #1027
2025-06-09 14:01:25 +07:00
devlikepro 502287a258 [core] WEBJS - add exposeFunctionIfAbsent 2025-06-09 14:01:25 +07:00
devlikepro 052ff0ea6e [core] WEBJS - fork-main-2025-06-09 branch 2025-06-09 14:01:24 +07:00
devlikepro 2b76789cdf [core] Run GOWS with pprof if WAHA_DEBUG_MODE=true 2025-06-09 14:01:24 +07:00
devlikepro 7bbf5df231 [core] Update grpc 2025-06-09 14:01:24 +07:00
devlikepro 794a04e7d3 [core] Update grpc-js and grpc-tools 2025-06-09 14:01:23 +07:00
devlikepro 645467f13a [core] echo -e 2025-06-09 14:01:23 +07:00
devlikepro 7e87637341 [core] 2025.5.6
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-05-30 11:18:16 +07:00
44 changed files with 3026 additions and 2675 deletions

No files matched your search

+1 -1
View File
@@ -1 +1 @@
v22.8
v22.16
+17 -2
View File
@@ -1,7 +1,7 @@
#
# Build
#
ARG NODE_VERSION=22.8-bullseye
ARG NODE_VERSION=22.16-bullseye
FROM node:${NODE_VERSION} AS build
ENV PUPPETEER_SKIP_DOWNLOAD=True
@@ -119,6 +119,21 @@ RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
&& rm -rf /var/lib/apt/lists/*; \
fi
# Install xvfb
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
apt-get update && apt-get install -y --no-install-recommends \
xvfb \
libnss3 \
libxss1 \
libasound2 \
libatk-bridge2.0-0 \
libgtk-3-0 \
libdrm2 \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*; \
fi
# Install Chromium
RUN if [ "$USE_BROWSER" = "chromium" ]; then \
apt-get update \
@@ -131,7 +146,7 @@ RUN if [ "$USE_BROWSER" = "chromium" ]; then \
# Install Chrome
# Available versions:
# https://www.ubuntuupdates.org/package/google_chrome/stable/main/base/google-chrome-stable
ARG CHROME_VERSION="135.0.7049.114-1"
ARG CHROME_VERSION="137.0.7151.103-1"
RUN if [ "$USE_BROWSER" = "chrome" ]; then \
wget --no-verbose -O /tmp/chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb \
&& apt-get update \
+1 -1
View File
@@ -41,7 +41,7 @@ up-noweb-libsignal:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up libsignal@github:devlikeapro/libsignal-node#fork-master
up-webjs:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-channels
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2025-06-09
start-proxy:
docker run --rm -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta
+1 -1
View File
@@ -146,7 +146,7 @@ curl -d "{\"chatId\": \"${PHONE}@c.us\", \"text\": \"Hello from WhatsApp HTTP AP
## Start the project
1. Clone the repository
2. Make sure you're using node>=22
2. Make sure you're using node>=22 (check [.nvmrc](/.nvmrc) to get the version)
3. Run the following commands:
```bash
# Install dependencies
+10
View File
@@ -1,5 +1,15 @@
#!/bin/sh
# Check if Xvfb command exists
if command -v Xvfb > /dev/null 2>&1; then
# Start virtual X server in the background
Xvfb :99 -screen 0 1280x720x24 &
export DISPLAY=:99
sleep 2
else
echo "Xvfb command not found, skipping virtual X server setup"
fi
# Calculate UV_THREADPOOL_SIZE based on number of CPUs
cpus=$(node -e "const os = require('os'); console.log(os.cpus().length);")
uv_threadpool_size=$(($cpus * 1))
Binary file not shown.
Binary file not shown.
+7 -6
View File
@@ -49,7 +49,7 @@
"agentkeepalive": "^4.5.0",
"async-lock": "^1.4.1",
"audio-decode": "^2.2.2",
"axios": "^1.7.7",
"axios": "^1.9.0",
"axios-retry": "^4.5.0",
"better-sqlite3": "11.3.0",
"check-disk-space": "^3.4.0",
@@ -88,7 +88,7 @@
"sharp": "^0.33.4",
"swagger-ui-express": "^4.1.4",
"ulid": "^2.3.0",
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-channels",
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2025-06-09",
"write-file-atomic": "^6.0.0"
},
"optionalDependencies": {
@@ -97,12 +97,13 @@
"resolutions": {
"bufferutil": "^4.0.8",
"ws": "^8.18.0",
"puppeteer": "^24.7.2",
"puppeteer": "^24.10.0",
"whatwg-url": "13.0.0",
"libsignal": "github:devlikeapro/libsignal-node#fork-master"
"libsignal": "github:devlikeapro/libsignal-node#fork-master",
"axios": "^1.9.0"
},
"devDependencies": {
"@grpc/grpc-js": "^1.12.4",
"@grpc/grpc-js": "^1.13.4",
"@grpc/proto-loader": "^0.7.13",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
@@ -117,7 +118,7 @@
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-simple-import-sort": "^10.0.0",
"grpc-tools": "^1.12.4",
"grpc-tools": "^1.13.0",
"jest": "^29.7.0",
"prettier": "^1.19.1",
"protoc-gen-ts": "^0.8.7",
+2 -2
View File
@@ -12,9 +12,9 @@ touch "$OUTPUT_FILE"
# Find all .md files in the source directory and combine them
find "$SOURCE_DIR" -type f -name "*.ts" | while read -r file; do
# Add file name to OUTPUT_FILE
echo -e "\n-------" >> "$OUTPUT_FILE"
echo "\n-------" >> "$OUTPUT_FILE"
echo "File: $file" >> "$OUTPUT_FILE"
echo -e "-------\n" >> "$OUTPUT_FILE"
echo "-------\n" >> "$OUTPUT_FILE"
# Append the content of the file to the output file
cat "$file" >> "$OUTPUT_FILE"
done
+2 -2
View File
@@ -36,8 +36,8 @@ class AuthController {
summary: 'Get QR code for pairing WhatsApp API.',
})
@SessionApiParam
@ApiFileAcceptHeader(Base64File, QRCodeValue)
@UseInterceptors(new BufferResponseInterceptor())
@ApiFileAcceptHeader('image/png', Base64File, QRCodeValue)
@UseInterceptors(new BufferResponseInterceptor('image/png'))
async getQR(
@QRCodeSessionParam session: WhatsappSession,
@Query() query: QRCodeQuery,
+83
View File
@@ -0,0 +1,83 @@
import {
Body,
Controller,
Post,
UnprocessableEntityException,
UseInterceptors,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { WAMimeType } from '@waha/core/media/WAMimeType';
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import {
FileDTO,
VideoFileDTO,
VoiceFileDTO,
} from '@waha/structures/media.dto';
import { WhatsappSession } from '../core/abc/session.abc';
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
@ApiSecurity('api_key')
@Controller('api/:session/media')
@ApiTags('🖼️ Media')
class MediaController {
constructor(private manager: SessionManager) {}
@Post('convert/voice')
@ApiOperation({
summary: 'Convert voice to WhatsApp format (opus)',
})
@SessionApiParam
@UseInterceptors(
new BufferResponseInterceptor(WAMimeType.VOICE, 'output.opus'),
)
@ApiFileAcceptHeader(WAMimeType.VOICE)
async convertVoice(
@WorkingSessionParam session: WhatsappSession,
@Body() file: VoiceFileDTO,
): Promise<Buffer> {
const data = await this.buffer(session, file);
const content = await session.mediaConverter.voice(data);
return content;
}
@Post('convert/video')
@ApiOperation({
summary: 'Convert video to WhatsApp format (mp4)',
})
@SessionApiParam
@UseInterceptors(
new BufferResponseInterceptor(WAMimeType.VIDEO, 'output.mp4'),
)
@ApiFileAcceptHeader(WAMimeType.VIDEO)
async convertVideo(
@WorkingSessionParam session: WhatsappSession,
@Body() file: VideoFileDTO,
): Promise<Buffer> {
const data = await this.buffer(session, file);
const content = await session.mediaConverter.video(data);
return content;
}
private async buffer(
session: WhatsappSession,
file: FileDTO,
): Promise<Buffer> {
if ('url' in file) {
return session.fetch(file.url);
} else if ('data' in file) {
return Buffer.from(file.data, 'base64');
} else {
throw new UnprocessableEntityException(
'Either "url" or "data" must be specified.',
);
}
}
}
export { MediaController };
+2 -2
View File
@@ -21,8 +21,8 @@ export class ScreenshotController {
constructor(private manager: SessionManager) {}
@Get('/screenshot')
@UseInterceptors(new BufferResponseInterceptor())
@ApiFileAcceptHeader()
@UseInterceptors(new BufferResponseInterceptor('image/jpeg'))
@ApiFileAcceptHeader('image/jpeg')
async screenshot(
@Res({ passthrough: true }) res: Response,
@Query() sessionQuery: SessionQuery,
+1
View File
@@ -76,6 +76,7 @@ export class SwaggerConfiguratorCore {
'🏷️ Labels',
'Labels - available only for WhatsApp Business accounts',
)
.addTag('🖼️ Media', 'Media methods')
.addTag('🔍 Observability', 'Other methods')
.addTag('🗄️ Storage', 'Storage methods')
.addApiKey({
+1
View File
@@ -57,6 +57,7 @@ export abstract class SessionManager
protected gowsConfigService: GowsEngineConfigService,
) {
this.lock = new AsyncLock({
timeout: 5_000,
maxPending: Infinity,
maxExecutionTime: 30_000,
});
+21 -4
View File
@@ -1,4 +1,8 @@
import { isJidBroadcast } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import {
CoreMediaConverter,
IMediaConverter,
} from '@waha/core/media/IConverter';
import { MessagesForRead } from '@waha/core/utils/convertors';
import { isJidNewsletter } from '@waha/core/utils/jids';
import {
@@ -24,16 +28,15 @@ import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto';
import { BinaryFile, RemoteFile } from '@waha/structures/files.dto';
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import {
LimitOffsetParams,
PaginationParams,
} from '@waha/structures/pagination.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { MessageSource, WAMessage } from '@waha/structures/responses.dto';
import { DefaultMap } from '@waha/utils/DefaultMap';
import { generatePrefixedId } from '@waha/utils/ids';
import { LoggerBuilder } from '@waha/utils/logging';
import { complete } from '@waha/utils/reactive/complete';
import { SwitchObservable } from '@waha/utils/reactive/SwitchObservable';
import axios from 'axios';
import axiosRetry from 'axios-retry';
import * as fs from 'fs';
import * as lodash from 'lodash';
import * as NodeCache from 'node-cache';
@@ -109,9 +112,12 @@ import { NotImplementedByEngineError } from '../exceptions';
import { IMediaManager } from '../media/IMediaManager';
import { QR } from '../QR';
import { DataStore } from './DataStore';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const qrcode = require('qrcode-terminal');
axiosRetry(axios, { retries: 3 });
const CHROME_PATH = '/usr/bin/google-chrome-stable';
const CHROMIUM_PATH = '/usr/bin/chromium';
@@ -168,6 +174,8 @@ export abstract class WhatsappSession {
stdTTL: 10 * 60, // 10 minutes
});
public mediaConverter: IMediaConverter = new CoreMediaConverter();
public constructor({
name,
printQR,
@@ -993,6 +1001,15 @@ export abstract class WhatsappSession {
const api = this.sentMessageIds.has(id);
return api ? MessageSource.API : MessageSource.APP;
}
/**
* Fetches the content from the specified URL and returns it as a Buffer.
*/
public async fetch(url: string): Promise<Buffer> {
return axios.get(url, { responseType: 'arraybuffer' }).then((res) => {
return Buffer.from(res.data);
});
}
}
export function getGroupInviteLink(code: string) {
+4 -4
View File
@@ -108,13 +108,13 @@ export class WebsocketGatewayCore
// Cherry-pick from nestjs new version
// https://github.com/nestjs/nest/pull/13531/files
private async close(server: any) {
const closeEventSignal = new Promise((resolve, reject) =>
server.close((err) => (err ? reject(err) : resolve(undefined))),
);
// const closeEventSignal = new Promise((resolve, reject) =>
// server.close((err) => (err ? reject(err) : resolve(undefined))),
// );
for (const ws of server.clients) {
ws.terminate();
}
await closeEventSignal;
// await closeEventSignal;
}
afterInit(server: Server) {
+2
View File
@@ -36,6 +36,7 @@ import { EventsController } from '../api/events.controller';
import { GroupsController } from '../api/groups.controller';
import { HealthController } from '../api/health.controller';
import { LabelsController } from '../api/labels.controller';
import { MediaController } from '../api/media.controller';
import { PingController } from '../api/ping.controller';
import { PresenceController } from '../api/presence.controller';
import { ScreenshotController } from '../api/screenshot.controller';
@@ -141,6 +142,7 @@ export const CONTROLLERS = [
ServerController,
ServerDebugController,
VersionController,
MediaController,
];
const PROVIDERS = [
{
+6 -1
View File
@@ -1,16 +1,21 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { WhatsappConfigService } from '@waha/config.service';
import { BootstrapConfig } from '@waha/core/engines/gows/GowsBootstrap';
import { GowsConfig } from '@waha/core/engines/gows/session.gows.core';
@Injectable()
export class GowsEngineConfigService {
constructor(protected configService: ConfigService) {}
constructor(
protected configService: ConfigService,
protected whatsappConfigService: WhatsappConfigService,
) {}
getBootstrapConfig(): BootstrapConfig {
return {
path: this.configService.get('WAHA_GOWS_PATH'),
socket: this.getSocket(),
pprof: this.whatsappConfigService.debugModeEnabled,
};
}
+2
View File
@@ -22,6 +22,7 @@ export async function isUnixSocket(socketPath: string): Promise<boolean> {
export interface BootstrapConfig {
path: string;
socket: string;
pprof?: boolean;
}
export class GowsBootstrap implements EngineBootstrap {
@@ -44,6 +45,7 @@ export class GowsBootstrap implements EngineBootstrap {
this.logger,
this.config.path,
this.config.socket,
this.config.pprof,
);
this.gows.start(() => {
this.logger.info(`GOWS stopped, exiting...`);
+10 -1
View File
@@ -14,13 +14,22 @@ export class GowsSubprocess {
private logger: Logger,
readonly path: string,
readonly socket: string,
readonly pprof: boolean = false,
) {}
start(onExit: (code: number) => void) {
this.logger.info('Starting GOWS subprocess...');
this.logger.debug(`GOWS path '${this.path}', socket: '${this.socket}'...`);
this.child = spawn(this.path, ['--socket', this.socket], {
const args = ['--socket', this.socket];
if (this.pprof) {
this.logger.info('Debug mode enabled, adding pprof flags');
args.push('--pprof');
args.push('--pprof-port=6060');
args.push('--pprof-host=0.0.0.0');
}
this.child = spawn(this.path, args, {
detached: true,
});
this.logger.debug(`GOWS started with PID: ${this.child.pid}`);
+2 -2
View File
@@ -671,7 +671,7 @@ var EventStreamService = exports.EventStreamService = {
},
};
exports.EventStreamClient = grpc.makeGenericClientConstructor(EventStreamService);
exports.EventStreamClient = grpc.makeGenericClientConstructor(EventStreamService, 'EventStream');
var MessageServiceService = exports.MessageServiceService = {
//
// Session management
@@ -1381,4 +1381,4 @@ getMessageById: {
},
};
exports.MessageServiceClient = grpc.makeGenericClientConstructor(MessageServiceService);
exports.MessageServiceClient = grpc.makeGenericClientConstructor(MessageServiceService, 'MessageService');
@@ -129,6 +129,8 @@ import {
EnginePayload,
PollVotePayload,
WAMessageAckBody,
WAMessageEditedBody,
WAMessageRevokedBody,
} from '@waha/structures/webhooks.dto';
import { PaginatorInMemory } from '@waha/utils/Paginator';
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
@@ -407,6 +409,53 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
this.events2.get(WAHAEvents.MESSAGE).switch(messagesFromOthers$);
this.events2.get(WAHAEvents.MESSAGE_ANY).switch(messagesFromAll$);
// Handle revoked messages
const messagesRevoked$ = messages$.pipe(
filter((msg) => {
return (
msg?.Message?.protocolMessage?.type === 0 &&
msg?.Message?.protocolMessage?.key !== undefined
);
}),
mergeMap(async (message): Promise<WAMessageRevokedBody> => {
const afterMessage = await this.toWAMessage(message);
// Extract the revoked message ID from protocolMessage.key
const revokedMessageId = message.Message.protocolMessage.key?.ID;
return {
after: afterMessage,
before: null,
revokedMessageId: revokedMessageId,
_data: message,
};
}),
);
this.events2.get(WAHAEvents.MESSAGE_REVOKED).switch(messagesRevoked$);
// Handle edited messages
const messagesEdited$ = messages$.pipe(
filter((msg) => {
return (
msg?.Message?.protocolMessage?.type === 14 &&
msg?.Message?.protocolMessage?.editedMessage !== undefined
);
}),
mergeMap(async (message): Promise<WAMessageEditedBody> => {
const waMessage = await this.toWAMessage(message);
// Extract the body from editedMessage using extractBody function
const body =
this.extractBody(message.Message.protocolMessage.editedMessage) || '';
// Extract the original message ID from protocolMessage.key
const editedMessageId = message.Message.protocolMessage.key?.ID;
return {
...waMessage,
body: body,
editedMessageId: editedMessageId,
_data: message,
};
}),
);
this.events2.get(WAHAEvents.MESSAGE_EDITED).switch(messagesEdited$);
const receipt$ = all$.pipe(onlyEvent(WhatsMeowEvent.RECEIPT));
const messageAck$ = receipt$.pipe(
mergeMap(this.receiptToMessageAck.bind(this)),
+40 -2
View File
@@ -159,6 +159,7 @@ import {
PollVote,
PollVotePayload,
WAMessageAckBody,
WAMessageEditedBody,
WAMessageRevokedBody,
} from '@waha/structures/webhooks.dto';
import { LoggerBuilder } from '@waha/utils/logging';
@@ -595,7 +596,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
if (
protocolMsg !== null &&
protocolMsg !== undefined &&
protocolMsg.editedMessage
protocolMsg.editedMessage &&
protocolMsg.key
) {
this.sock?.ev.emit('messages.update', [
{
@@ -1829,15 +1831,45 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
),
mergeMap(async (message): Promise<WAMessageRevokedBody> => {
const afterMessage = await this.toWAMessage(message);
// Extract the revoked message ID from protocolMessage.key
const revokedMessageId = message.message.protocolMessage.key?.id;
return {
after: afterMessage,
before: null,
revokedMessageId: revokedMessageId,
_data: message,
};
}),
);
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,
),
mergeMap(async (message): Promise<WAMessageEditedBody> => {
const waMessage = await this.toWAMessage(message);
// Extract the body from editedMessage using extractBody function
const body =
this.extractBody(message.message.protocolMessage.editedMessage) || '';
// Extract the original message ID from protocolMessage.key
const editedMessageId = message.message.protocolMessage.key?.id;
return {
...waMessage,
body: body,
editedMessageId: editedMessageId,
_data: message,
};
}),
);
this.events2.get(WAHAEvents.MESSAGE_EDITED).switch(messagesEdited$);
//
// Message Reactions
//
@@ -2101,12 +2133,18 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
if (message.message.pollUpdateMessage) return;
// Ignore calls, we have dedicated handler for that
if (message.message.call?.callKey) return;
// Ignore revoke, we have a dedicated handler for that
// Ignore revoke, we have a dedicated event for that
if (
message.message?.protocolMessage?.type ===
proto.Message.ProtocolMessage.Type.REVOKE
)
return;
// Ignore edit, we have a dedicated event for that
if (
message.message?.protocolMessage?.type ===
proto.Message.ProtocolMessage.Type.MESSAGE_EDIT
)
return;
if (
message.message?.protocolMessage?.type ===
proto.Message.ProtocolMessage.Type.EPHEMERAL_SYNC_RESPONSE
@@ -64,11 +64,13 @@ export class NowebPersistentStore implements INowebStore {
public presences: any;
private lock: any = new AsyncLock({
timeout: 5_000,
maxPending: Infinity,
maxExecutionTime: 60_000,
});
private groupsFetchLock: any = new AsyncLock({
timeout: 5_000,
maxPending: Infinity,
maxExecutionTime: 60_000,
});
@@ -19,6 +19,7 @@ const AsyncLock = require('async-lock');
// Default pending is 1000, set it to infinity
// https://github.com/rogierschouten/async-lock/issues/63
const fileLock = new AsyncLock({
timeout: 5_000,
maxPending: Infinity,
maxExecutionTime: 30_000,
});
+21
View File
@@ -0,0 +1,21 @@
/**
* Expose a function to the page if it does not exist
*
* NOTE:
* Rewrite it to 'upsertFunction' after updating Puppeteer to 20.6 or higher
* using page.removeExposedFunction
* https://pptr.dev/api/puppeteer.page.removeExposedFunction
*
* @param {import(puppeteer).Page} page
* @param {string} name
* @param {Function} fn
*/
export async function exposeFunctionIfAbsent(page, name, fn) {
const exist = await page.evaluate((name) => {
return !!window[name];
}, name);
if (exist) {
return;
}
await page.exposeFunction(name, fn);
}
+1 -1
View File
@@ -1,3 +1,4 @@
import { exposeFunctionIfAbsent } from '@waha/core/engines/webjs/Puppeteer';
import { WebJSPresence } from '@waha/core/engines/webjs/types';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { Label } from '@waha/structures/labels.dto';
@@ -10,7 +11,6 @@ import * as lodash from 'lodash';
import { Page } from 'puppeteer';
import { Client, Events } from 'whatsapp-web.js';
import { Message } from 'whatsapp-web.js/src/structures';
import { exposeFunctionIfAbsent } from 'whatsapp-web.js/src/util/Puppeter';
import { CallErrorEvent, PAGE_CALL_ERROR_EVENT, WPage } from './WPage';
@@ -111,6 +111,7 @@ import { StatusRequest, TextStatus } from '@waha/structures/status.dto';
import {
EnginePayload,
WAMessageAckBody,
WAMessageEditedBody,
WAMessageRevokedBody,
} from '@waha/structures/webhooks.dto';
import { PaginatorInMemory } from '@waha/utils/Paginator';
@@ -1418,9 +1419,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
const beforeMessage = event.before
? this.toWAMessage(event.before)
: null;
// Extract the revoked message ID from the protocolMessageKey.id field
const revokedMessageId = afterMessage?._data?.protocolMessageKey?.id;
return {
after: afterMessage,
before: beforeMessage,
revokedMessageId: revokedMessageId,
};
}),
);
@@ -1432,6 +1436,26 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
);
this.events2.get(WAHAEvents.MESSAGE_REACTION).switch(messagesReaction$);
const messageEdit$ = fromEvent(
this.whatsapp,
Events.MESSAGE_EDIT,
(message, newBody, prevBody) => {
return { message, newBody, prevBody };
},
);
const messagesEdit$ = messageEdit$.pipe(
map((event): WAMessageEditedBody => {
const message = this.toWAMessage(event.message);
return {
...message,
body: event.newBody,
editedMessageId: message._data?.id?.id,
_data: event,
};
}),
);
this.events2.get(WAHAEvents.MESSAGE_EDITED).switch(messagesEdit$);
const messageAckWEBJS$ = fromEvent(
this.whatsapp,
Events.MESSAGE_ACK,
+16
View File
@@ -0,0 +1,16 @@
import { AvailableInPlusVersion } from '../exceptions';
export interface IMediaConverter {
voice(content: Buffer): Promise<Buffer>;
video(content: Buffer): Promise<Buffer>;
}
export class CoreMediaConverter implements IMediaConverter {
video(content: Buffer): Promise<Buffer> {
throw new AvailableInPlusVersion();
}
voice(content: Buffer): Promise<Buffer> {
throw new AvailableInPlusVersion();
}
}
+4
View File
@@ -0,0 +1,4 @@
export enum WAMimeType {
VOICE = 'audio/ogg; codecs=opus',
VIDEO = 'video/mp4',
}
+2 -4
View File
@@ -79,15 +79,13 @@ export class SqlKVRepository<Entity> {
}
}
private async upsertBatch(entities: Entity[]): Promise<void> {
protected async upsertBatch(entities: Entity[]): Promise<void> {
const all = entities.map((entity) => this.dump(entity));
// make it unique by .id
const data = lodash.uniqBy(all, (d: any) => d.id);
if (data.length != all.length) {
console.warn(
`WARNING - Duplicated entities for upsert batch: ${JSON.stringify(
entities,
)}`,
`WARNING - Duplicated entities for upsert batch: all=${all.length}, data=${data.length}`,
);
}
const columns = this.columns.map((c) => `"${c.fieldName}"`);
@@ -1,6 +1,7 @@
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
import { Sqlite3Engine } from '@waha/core/storage/sqlite3/Sqlite3Engine';
import { Sqlite3JsonQuery } from '@waha/core/storage/sqlite3/Sqlite3JsonQuery';
import { sleep } from '@waha/utils/promiseTimeout';
import { Database } from 'better-sqlite3';
import Knex from 'knex';
@@ -18,4 +19,12 @@ export class Sqlite3KVRepository<Entity> extends SqlKVRepository<Entity> {
super(engine, knex);
this.db = db;
}
protected async upsertBatch(entities: Entity[]): Promise<void> {
await super.upsertBatch(entities);
// Give some time to the Node.js loop because we're using sync better-sqlite
if (entities.length >= this.UPSERT_BATCH_SIZE) {
await sleep(1);
}
}
}
+2 -2
View File
@@ -11,7 +11,7 @@ function getRefSchemaPaths(models) {
/**
* Decorator to add a file accept header to the swagger documentation
*/
export function ApiFileAcceptHeader(...models) {
export function ApiFileAcceptHeader(mimetype: string, ...models) {
models = models.length ? models : [Base64File];
return applyDecorators(
// Add extra models, otherwise it'll give a error
@@ -20,7 +20,7 @@ export function ApiFileAcceptHeader(...models) {
ApiResponse({
status: 200,
content: {
'image/png': {
[mimetype]: {
schema: {
type: 'string',
format: 'binary',
+32 -19
View File
@@ -21,6 +21,11 @@ import { Base64File } from '../structures/files.dto';
export class BufferResponseInterceptor<T>
implements NestInterceptor<Buffer, Base64File | StreamableFile>
{
constructor(
private mimetype: string,
private filename: string = null,
) {}
intercept(
context: ExecutionContext,
next: CallHandler,
@@ -28,27 +33,35 @@ export class BufferResponseInterceptor<T>
const ctx = context.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
return next.handle().pipe(map(processBuffer));
function processBuffer(buffer: Buffer) {
// Check buffer is Buffer
if (!Buffer.isBuffer(buffer)) {
return buffer;
}
return next
.handle()
.pipe(map((buffer) => this.processBuffer(buffer, request, response)));
}
const accept = request.headers['accept'];
if (accept == 'application/json') {
return {
mimetype: 'image/png',
data: buffer.toString('base64'),
};
}
const file = new StreamableFile(buffer);
response.set({
'Content-Type': 'image/png',
'Content-Length': buffer.length,
});
return file;
processBuffer(buffer: Buffer, request: Request, response: Response) {
// Check buffer is Buffer
if (!Buffer.isBuffer(buffer)) {
return buffer;
}
const accept = request.headers['accept'];
if (accept == 'application/json') {
return {
mimetype: this.mimetype,
data: buffer.toString('base64'),
};
}
const file = new StreamableFile(buffer);
response.set({
'Content-Type': this.mimetype,
'Content-Length': buffer.length,
});
if (this.filename) {
response.set({
'Content-Disposition': `attachment; filename="${this.filename}"`,
});
}
return file;
}
}
+11 -1
View File
@@ -32,7 +32,11 @@ import {
VoiceBinaryFile,
VoiceRemoteFile,
} from './files.dto';
import { ChatIdProperty, ReplyToProperty } from './properties.dto';
import {
ChatIdProperty,
ConvertApiProperty,
ReplyToProperty,
} from './properties.dto';
/**
* Queries
@@ -320,6 +324,9 @@ export class MessageVoiceRequest extends ChatRequest {
@ReplyToProperty()
reply_to?: string;
@ConvertApiProperty()
convert: boolean;
}
@ApiExtraModels(VideoRemoteFile, VideoBinaryFile)
@@ -346,6 +353,9 @@ export class MessageVideoRequest extends ChatRequest {
example: false,
})
asNote?: boolean;
@ConvertApiProperty()
convert: boolean;
}
export class MessageLinkPreviewRequest extends ChatRequest {
+1
View File
@@ -8,6 +8,7 @@ export enum WAHAEvents {
MESSAGE_ACK = 'message.ack',
MESSAGE_WAITING = 'message.waiting',
MESSAGE_REVOKED = 'message.revoked',
MESSAGE_EDITED = 'message.edited',
STATE_CHANGE = 'state.change',
GROUP_JOIN = 'group.join',
GROUP_LEAVE = 'group.leave',
+31
View File
@@ -35,3 +35,34 @@ export class WAMedia {
// eslint-disable-next-line @typescript-eslint/ban-types
error?: object;
}
export class FileDTO {
@ApiProperty({
description: 'The URL for the file',
})
url?: string;
@ApiProperty({
description: 'Base64 content of the file',
example: null,
})
data?: string;
}
export class VoiceFileDTO extends FileDTO {
@ApiProperty({
description: 'The URL for the voice file',
example:
process.env.WHATSAPP_SWAGGER_VIDEO_EXAMPLE_URL ||
'https://github.com/devlikeapro/waha/raw/core/examples/voice.mp3',
})
url?: string;
}
export class VideoFileDTO extends FileDTO {
@ApiProperty({
description: 'The URL for the video file',
example: 'https://github.com/devlikeapro/waha/raw/core/examples/video.mp4',
})
url?: string;
}
+8
View File
@@ -30,6 +30,14 @@ function ReplyToProperty() {
});
}
export function ConvertApiProperty() {
return ApiProperty({
description:
'Convert the input file to the required format using ffmpeg before sending',
example: true,
});
}
export {
ChatIdProperty,
MessageIdOnlyProperty,
+7
View File
@@ -1,4 +1,5 @@
import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger';
import { ConvertApiProperty } from '@waha/structures/properties.dto';
import {
BinaryFile,
@@ -63,6 +64,9 @@ export class VoiceStatus extends StatusRequest {
file: VoiceRemoteFile | VoiceBinaryFile;
backgroundColor: string = '#38b42f';
@ConvertApiProperty()
convert: boolean;
}
@ApiExtraModels(VideoRemoteFile, VideoBinaryFile)
@@ -76,6 +80,9 @@ export class VideoStatus extends StatusRequest {
file: VideoRemoteFile | VideoBinaryFile;
caption?: string;
@ConvertApiProperty()
convert: boolean;
}
export class DeleteStatusRequest extends StatusRequest {
+25
View File
@@ -93,9 +93,24 @@ export class PollVotePayload {
export class WAMessageRevokedBody {
after?: WAMessage;
before?: WAMessage;
@ApiProperty({
description: 'ID of the message that was revoked',
example: 'A06CA7BB5DD8C8F705628CDB7E3A33C9',
})
revokedMessageId?: string;
_data?: any;
}
export class WAMessageEditedBody extends WAMessage {
@ApiProperty({
description: 'ID of the original message that was edited',
example: 'A06CA7BB5DD8C8F705628CDB7E3A33C9',
})
editedMessageId?: string;
}
export class WASessionStatusBody {
@ApiProperty({
example: 'default',
@@ -206,6 +221,16 @@ export class WAHAWebhookMessageRevoked extends WAHAWebhook {
payload: WAMessageRevokedBody;
}
export class WAHAWebhookMessageEdited extends WAHAWebhook {
@ApiProperty({
description:
'The event is triggered when a user edits a previously sent message.',
})
event = WAHAEvents.MESSAGE_EDITED;
payload: WAMessageEditedBody;
}
export class WAHAWebhookStateChange extends WAHAWebhook {
@ApiProperty({
description: 'It’s an internal engine’s state, not session status.',
+2
View File
@@ -21,6 +21,7 @@ import {
WAHAWebhookMessage,
WAHAWebhookMessageAck,
WAHAWebhookMessageAny,
WAHAWebhookMessageEdited,
WAHAWebhookMessageReaction,
WAHAWebhookMessageRevoked,
WAHAWebhookPollVote,
@@ -37,6 +38,7 @@ const WAHA_WEBHOOKS = [
WAHAWebhookMessageAny,
WAHAWebhookMessageAck,
WAHAWebhookMessageRevoked,
WAHAWebhookMessageEdited,
WebhookGroupV2Join,
WebhookGroupV2Leave,
WebhookGroupV2Update,
+3 -1
View File
@@ -33,9 +33,11 @@ export function getEngineName(): string {
}
export const VERSION: WAHAEnvironment = {
version: '2025.5.5',
version: '2025.6.4',
engine: getEngineName(),
tier: getWAHAVersion(),
browser:
getEngineName() === WAHAEngine.WEBJS ? getBrowserExecutablePath() : null,
};
export const IsChrome = VERSION.browser?.includes('chrome');
+1 -1
View File
@@ -2,7 +2,7 @@
"waha": {
"gows": {
"repo": "devlikeapro/gows",
"ref": "v1.0.7"
"ref": "v1.0.8"
},
"dashboard": {
"repo": "devlikeapro/dashboard",
+2559 -2615
View File
File diff suppressed because it is too large. Load diff