Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b6fa3343d | ||
|
|
469cdc9b1d | ||
|
|
8a179e7194 | ||
|
|
c133795753 | ||
|
|
941ace7461 | ||
|
|
8589420f5d | ||
|
|
ece24e182f | ||
|
|
38e5bb7bdf | ||
|
|
d09e931827 | ||
|
|
3f3f1e1c20 | ||
|
|
32f2468ba8 | ||
|
|
1d6540e0d9 | ||
|
|
31da4bfca1 | ||
|
|
41d49894e2 | ||
|
|
6dcdd15b1c | ||
|
|
c2b275fd90 | ||
|
|
e4838ef3ef | ||
|
|
55ca4660d7 | ||
|
|
c16c903ec4 | ||
|
|
937cf38ff6 | ||
|
|
07f76e854d | ||
|
|
cde1d084c6 | ||
|
|
9362c5d6ae | ||
|
|
117a9108c0 | ||
|
|
5214e461f2 | ||
|
|
9bdb178b7d | ||
|
|
9c526ffdba | ||
|
|
f80a74edd1 |
No files matched your search
+17
-2
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
+4
-3
@@ -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",
|
||||
@@ -97,9 +97,10 @@
|
||||
"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.13.4",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -57,6 +57,7 @@ export abstract class SessionManager
|
||||
protected gowsConfigService: GowsEngineConfigService,
|
||||
) {
|
||||
this.lock = new AsyncLock({
|
||||
timeout: 5_000,
|
||||
maxPending: Infinity,
|
||||
maxExecutionTime: 30_000,
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum WAMimeType {
|
||||
VOICE = 'audio/ogg; codecs=opus',
|
||||
VIDEO = 'video/mp4',
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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
@@ -33,9 +33,11 @@ export function getEngineName(): string {
|
||||
}
|
||||
|
||||
export const VERSION: WAHAEnvironment = {
|
||||
version: '2025.6.1',
|
||||
version: '2025.6.4',
|
||||
engine: getEngineName(),
|
||||
tier: getWAHAVersion(),
|
||||
browser:
|
||||
getEngineName() === WAHAEngine.WEBJS ? getBrowserExecutablePath() : null,
|
||||
};
|
||||
|
||||
export const IsChrome = VERSION.browser?.includes('chrome');
|
||||
Reference in new issue
Block a user