Compare commits

..
42 Commits
Author SHA1 Message Date
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
devlikepro b7abeed54a [core] 2025.5.5
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-28 16:42:04 +07:00
devlikepro d53646eef2 [core] docker-compose.yaml - set logging by default 100m/10 files 2025-05-28 16:42:04 +07:00
devlikepro c54743de6c [core] Up GOWS 2025-05-28 16:42:03 +07:00
devlikepro e1fdc41132 [core] NOWEB - skip updates with no id 2025-05-28 16:42:03 +07:00
devlikepro 060aa82294 [core] WEBJS - Presence - API and Events
fix #636
2025-05-28 16:42:02 +07:00
devlikepro 2df08eac3f [core] NOWEB - hosted account pairing
fix #864
2025-05-28 16:42:02 +07:00
devlikepro 0ecdb9d3d0 [core] GOWS - Always subscribe to presence 2025-05-28 16:42:02 +07:00
devlikepro 6a7fe1bd1f [core] NOWEB - do not resubscribe to presences 2025-05-28 16:42:02 +07:00
devlikepro 87c69cb3d0 [core] NOWEB - Always subscribe to presence 2025-05-28 16:42:01 +07:00
devlikepro c654855c22 [core] Add ids filter to /overview
fix #791
2025-05-28 16:42:00 +07:00
devlikepro 11bb006801 [core] WEBJS - adjust logging on stop 2025-05-28 16:42:00 +07:00
devlikepro e5551f3f64 [core] Refactor path handling to improve security and safety.
Replaced `path.join` with `safeJoin` to validate and normalize paths, ensuring they stay within the base directory and prevent malicious access. Added `safeJoin` implementation in `utils/files.ts`, enforcing strict path validation rules. This improves robustness against path traversal vulnerabilities.
2025-05-28 16:41:59 +07:00
devlikepro 0de148005f [core] require session "name" for deprecated API 2025-05-28 16:41:58 +07:00
devlikepro 27e4ce628e [core] NOWEB - handle no content found messages: TypeError: Cannot read properties of undefined (reading 'pollUpdateMessage') 2025-05-28 16:41:58 +07:00
devlikepro 19b752bf15 [core] WEBJS - avoid "Can not get engine info" warning in logs 2025-05-28 16:41:58 +07:00
devlikepro 6528e5bba9 [core] WEBJS - hide "Fresh look" modal form
fix #987
2025-05-28 16:41:57 +07:00
devlikepro 37e1183e87 [core] Update nestjs 2025-05-28 16:41:56 +07:00
devlikepro 824512cf8e [core] add tini to Dockerfile
fix #990
2025-05-28 16:41:56 +07:00
devlikepro 82f296b17d [core] GOWS - Polls
fix devlikeapro/waha#899
2025-05-28 16:41:56 +07:00
devlikepro 8302530954 [core] 2025.5.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-05-23 14:47:37 +07:00
devlikepro 6c271829aa [core] Up Dashboard 2025-05-23 14:47:37 +07:00
devlikepro 3b30d339ee [core] Up GOWS 2025-05-23 14:47:37 +07:00
devlikepro c5a268bdbd [core] Up NOWEB - lid support
fix #957
2025-05-23 14:47:36 +07:00
devlikepro 8457ea8e47 [core] Find a phone number by lid - do not require @lid 2025-05-23 14:47:36 +07:00
devlikepro 72d69eeab5 [core] Add lid to phone number mapping - all engines
fix #957
2025-05-23 14:47:35 +07:00
devlikepro 31885e41af [core] Use maxExecutionTime instead of timeout in async-lock to avoid "async-lock timed out in queue" error
fix #831
2025-05-23 14:47:34 +07:00
devlikepro e63726d18b [core] GOWS - WhatsApp Event Message 2025-05-23 14:47:33 +07:00
devlikepro 3d2ab45d25 [core] GOWS - Labels support
fix #922
2025-05-23 14:47:33 +07:00
devlikepro 246478e1d8 [core] GOWS - add send vcard
fix #902
2025-05-23 14:47:32 +07:00
devlikepro 9418de3b23 [core] add reply_to to sendContactVcard 2025-05-23 14:47:32 +07:00
devlikepro c6b68cad37 [core] add types for toVcard 2025-05-23 14:47:32 +07:00
61 changed files with 8414 additions and 941 deletions

No files matched your search

+5
View File
@@ -146,6 +146,9 @@ RUN apt-get update \
&& apt-get install -y libc6 \
&& rm -rf /var/lib/apt/lists/*
# Install tini for proper init process
RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*
# Set the ENV for docker image
ENV WHATSAPP_DEFAULT_ENGINE=$WHATSAPP_DEFAULT_ENGINE
@@ -170,4 +173,6 @@ ENV WAHA_ZIPPER=ZIPUNZIP
# Run command, etc
EXPOSE 3000
# Use tini as init system to handle zombie processes properly
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/entrypoint.sh"]
+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
+21
View File
@@ -8,6 +8,12 @@ services:
# WAHA Core
# image: devlikeapro/waha:latest
logging:
driver: 'json-file'
options:
max-size: '100m'
max-file: '10'
ports:
- '127.0.0.1:3000:3000/tcp'
@@ -39,6 +45,11 @@ services:
# - postgres
# - "-c"
# - "max_connections=3000"
# logging:
# driver: "json-file"
# options:
# max-size: "100m"
# max-file: "10"
# NOTE: Only if you're using MongoDB
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
@@ -53,6 +64,11 @@ services:
# environment:
# - MONGO_INITDB_ROOT_USERNAME=mongouser
# - MONGO_INITDB_ROOT_PASSWORD=mongopassword
# logging:
# driver: "json-file"
# options:
# max-size: "100m"
# max-file: "10"
# NOTE: Only if you're using S3 to save media files
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
@@ -71,6 +87,11 @@ services:
# volumes:
# - minio_data:/data
# command: server /data --console-address ":9001"
# logging:
# driver: "json-file"
# options:
# max-size: "100m"
# max-file: "10"
volumes:
mongodb_data: {}
+1 -1
View File
@@ -7,5 +7,5 @@ uv_threadpool_size=$(($cpus * 1))
# Set UV_THREADPOOL_SIZE as an environment variable
export UV_THREADPOOL_SIZE="${UV_THREADPOOL_SIZE:-$uv_threadpool_size}"
# Start your application using yarn
# Start your application using node with exec to ensure proper signal handling
exec node dist/main
+15 -14
View File
@@ -31,18 +31,19 @@
"@aws-sdk/client-s3": "^3.633.0",
"@aws-sdk/s3-request-presigner": "^3.633.0",
"@nestjs/axios": "^3.0.2",
"@nestjs/common": "^9.0.9",
"@nestjs/common": "^11.0.0",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^9.0.9",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.0.9",
"@nestjs/platform-ws": "^9.0.9",
"@nestjs/serve-static": "^2.1.3",
"@nestjs/core": "^11.0.0",
"@nestjs/passport": "^11.0.0",
"@nestjs/platform-express": "^11.0.0",
"@nestjs/platform-ws": "^11.0.0",
"@nestjs/serve-static": "^5.0.0",
"@nestjs/swagger": "^7.1.11",
"@nestjs/terminus": "^10.2.3",
"@nestjs/websockets": "^9.0.9",
"@nestjs/websockets": "^11.0.0",
"@types/better-sqlite3": "^7.6.10",
"@types/lodash": "^4.14.194",
"@types/passport": "^1.0.17",
"@types/ws": "^8.5.4",
"adm-zip": "0.5.10",
"agentkeepalive": "^4.5.0",
@@ -71,7 +72,7 @@
"mongodb": "6.9.0",
"nestjs-pino": "^4.1.0",
"node-cache": "5.1.2",
"passport": "^0.6.0",
"passport": "^0.7.0",
"passport-headerapikey": "^1.2.2",
"pg": "^8.13.1",
"pino-http": "^10.2.0",
@@ -87,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": {
@@ -101,11 +102,11 @@
"libsignal": "github:devlikeapro/libsignal-node#fork-master"
},
"devDependencies": {
"@grpc/grpc-js": "^1.12.4",
"@grpc/grpc-js": "^1.13.4",
"@grpc/proto-loader": "^0.7.13",
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.1",
"@nestjs/testing": "^9.0.9",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.0",
"@types/express": "^4.17.3",
"@types/jest": "26.0.10",
"@types/node": "^18.14.2",
@@ -116,7 +117,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
+18 -1
View File
@@ -30,6 +30,8 @@ import {
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
OverviewBodyRequest,
OverviewFilter,
OverviewPaginationParams,
PinMessageRequest,
ReadChatMessagesQuery,
@@ -65,8 +67,23 @@ class ChatsController {
getChatsOverview(
@WorkingSessionParam session: WhatsappSession,
@Query() pagination: OverviewPaginationParams,
@Query() filter: OverviewFilter,
): Promise<ChatSummary[]> {
return session.getChatsOverview(pagination);
return session.getChatsOverview(pagination, filter);
}
@Post('overview')
@SessionApiParam
@ApiOperation({
summary:
'Get chats overview. Use POST if you have too many "ids" params - GET can limit it',
})
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
postChatsOverview(
@WorkingSessionParam session: WhatsappSession,
@Body() body: OverviewBodyRequest,
): Promise<ChatSummary[]> {
return session.getChatsOverview(body.pagination, body.filter);
}
@Delete(':chatId')
+50
View File
@@ -0,0 +1,50 @@
import {
Body,
Controller,
Param,
Post,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { SessionManager } from '../core/abc/manager.abc';
import {
EventCancelRequest,
EventMessageRequest,
} from '../structures/events.dto';
import { WAMessage } from '../structures/responses.dto';
@ApiSecurity('api_key')
@Controller('api/:session/events')
@ApiTags('📅 Events')
export class EventsController {
constructor(private manager: SessionManager) {}
@Post()
@ApiOperation({ summary: 'Send an event message' })
@UsePipes(new ValidationPipe())
@SessionApiParam
async sendEvent(
@WorkingSessionParam session: WhatsappSession,
@Body() request: EventMessageRequest,
): Promise<WAMessage> {
return session.sendEvent(request);
}
// @Post(':id/cancel')
// @ApiOperation({ summary: 'Cancel an event by ID' })
// @UsePipes(new ValidationPipe())
// @SessionApiParam
// async cancelEvent(
// @WorkingSessionParam session: WhatsappSession,
// @Param('id') id: string,
// ): Promise<WAMessage> {
// return session.cancelEvent(id);
// }
}
+102
View File
@@ -0,0 +1,102 @@
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import {
Controller,
Get,
Param,
Query,
UnprocessableEntityException,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { CountResponse } from '@waha/structures/base.dto';
import {
LidsListQueryParams,
LidToPhoneNumber,
} from '@waha/structures/lids.dto';
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
import { SessionManager } from '../core/abc/manager.abc';
@ApiSecurity('api_key')
@Controller('api/:session/lids')
@ApiTags('👤 Contacts')
export class LidsController {
constructor(private manager: SessionManager) {}
@Get('/')
@SessionApiParam
@ApiOperation({ summary: 'Get all known lids to phone number mapping' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getAll(
@WorkingSessionParam session: WhatsappSession,
@Query() params: LidsListQueryParams,
): Promise<Array<LidToPhoneNumber>> {
// Always lid
const pagination: PaginationParams = params;
pagination.sortBy = 'lid';
pagination.sortOrder = SortOrder.ASC;
const lids = await session.getAllLids(pagination);
return lids;
}
@Get('/count')
@SessionApiParam
@ApiOperation({ summary: 'Get the number of known lids' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getLidsCount(
@WorkingSessionParam session: WhatsappSession,
): Promise<CountResponse> {
const count = await session.getLidsCount();
return {
count: count,
};
}
@Get('/:lid')
@SessionApiParam
@ApiOperation({ summary: 'Get phone number by lid' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async findPNByLid(
@WorkingSessionParam session: WhatsappSession,
@Param('lid') lid: string,
): Promise<LidToPhoneNumber> {
if (!lid.includes('@')) {
lid = lid + '@lid';
}
if (!isLidUser(lid)) {
throw new UnprocessableEntityException(
'Invalid LID - it must end with @lid',
);
}
const result = await session.findPNByLid(lid);
result.pn = result.pn || null;
return result;
}
@Get('/pn/:phoneNumber')
@SessionApiParam
@ApiOperation({ summary: 'Get lid by phone number (chat id)' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async findLIDByPhoneNumber(
@WorkingSessionParam session: WhatsappSession,
@Param('phoneNumber') phoneNumber: string,
): Promise<LidToPhoneNumber> {
if (isLidUser(phoneNumber)) {
return {
lid: phoneNumber,
pn: null,
};
}
const result = await session.findLIDByPhoneNumber(phoneNumber);
result.lid = result.lid || null;
return result;
}
}
+9
View File
@@ -233,6 +233,9 @@ class SessionsController {
@Body() request: SessionStartDeprecatedRequest,
): Promise<SessionDTO> {
const name = request.name;
if (!request.name) {
throw new UnprocessableEntityException('Session name is required');
}
if (this.manager.isRunning(name)) {
const msg = `Session '${name}' is already started.`;
throw new UnprocessableEntityException(msg);
@@ -255,6 +258,9 @@ class SessionsController {
async DEPRECATED_stop(
@Body() request: SessionStopDeprecatedRequest,
): Promise<void> {
if (!request.name) {
throw new UnprocessableEntityException('Session name is required');
}
const name = request.name;
if (request.logout) {
// Old API did remove the session complete
@@ -283,6 +289,9 @@ class SessionsController {
async DEPRECATED_logout(
@Body() request: SessionLogoutDeprecatedRequest,
): Promise<void> {
if (!request.name) {
throw new UnprocessableEntityException('Session name is required');
}
const name = request.name;
await this.withLock(name, async () => {
await this.manager.unassign(name);
+1
View File
@@ -71,6 +71,7 @@ export class SwaggerConfiguratorCore {
)
.addTag('👥 Groups', `Groups methods.<br>`)
.addTag('✅ Presence', `Presence information`)
.addTag('📅 Events', `Event Message`)
.addTag(
'🏷️ Labels',
'Labels - available only for WhatsApp Business accounts',
+1 -2
View File
@@ -50,7 +50,6 @@ export abstract class SessionManager
WAIT_SESSION_RUNNING_TIMEOUT = 5_000;
WAIT_STATUS_INTERVAL = 500;
WAIT_STATUS_TIMEOUT = 10_000;
LOCK_TIMEOUT = 10_000;
protected constructor(
protected log: PinoLogger,
@@ -59,7 +58,7 @@ export abstract class SessionManager
) {
this.lock = new AsyncLock({
maxPending: Infinity,
timeout: this.LOCK_TIMEOUT,
maxExecutionTime: 30_000,
});
this.log.setContext(SessionManager.name);
}
+42 -1
View File
@@ -16,13 +16,18 @@ import {
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
OverviewFilter,
ReadChatMessagesQuery,
ReadChatMessagesResponse,
} from '@waha/structures/chats.dto';
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 { PaginationParams } from '@waha/structures/pagination.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import {
LimitOffsetParams,
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';
@@ -75,6 +80,10 @@ import {
WAHAPresenceStatus,
WAHASessionStatus,
} from '../../structures/enums.dto';
import {
EventCancelRequest,
EventMessageRequest,
} from '../../structures/events.dto';
import {
CreateGroupRequest,
GroupField,
@@ -466,6 +475,14 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
sendEvent(request: EventMessageRequest): Promise<WAMessage> {
throw new NotImplementedByEngineError();
}
cancelEvent(eventId: string): Promise<WAMessage> {
throw new NotImplementedByEngineError();
}
/**
* Chats methods
*/
@@ -475,6 +492,7 @@ export abstract class WhatsappSession {
public getChatsOverview(
pagination: PaginationParams,
filter?: OverviewFilter,
): Promise<ChatSummary[]> {
throw new NotImplementedByEngineError();
}
@@ -615,6 +633,29 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
/**
* Lid to Phone Number methods
*/
public async getAllLids(
pagination: PaginationParams,
): Promise<Array<LidToPhoneNumber>> {
throw new NotImplementedByEngineError();
}
public async getLidsCount(): Promise<number> {
throw new NotImplementedByEngineError();
}
public async findPNByLid(lid: string): Promise<LidToPhoneNumber> {
throw new NotImplementedByEngineError();
}
public async findLIDByPhoneNumber(
phoneNumber: string,
): Promise<LidToPhoneNumber> {
throw new NotImplementedByEngineError();
}
/**
* Fetch the latest profile picture of the contact (group, newsletter, etc.)
* @param id
+4
View File
@@ -5,6 +5,7 @@ import { PassportModule } from '@nestjs/passport';
import { ServeStaticModule } from '@nestjs/serve-static';
import { TerminusModule } from '@nestjs/terminus';
import { ChannelsController } from '@waha/api/channels.controller';
import { LidsController } from '@waha/api/lids.controller';
import { ProfileController } from '@waha/api/profile.controller';
import {
ServerController,
@@ -31,6 +32,7 @@ import { AuthController } from '../api/auth.controller';
import { ChatsController } from '../api/chats.controller';
import { ChattingController } from '../api/chatting.controller';
import { ContactsController } from '../api/contacts.controller';
import { EventsController } from '../api/events.controller';
import { GroupsController } from '../api/groups.controller';
import { HealthController } from '../api/health.controller';
import { LabelsController } from '../api/labels.controller';
@@ -129,9 +131,11 @@ export const CONTROLLERS = [
StatusController,
LabelsController,
ContactsController,
LidsController,
GroupsController,
PresenceController,
ScreenshotController,
EventsController,
PingController,
HealthController,
ServerController,
+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}`);
+3
View File
@@ -0,0 +1,3 @@
export function isFromFullSync(event: any): boolean {
return event.FromFullSync;
}
File diff suppressed because it is too large. Load diff
+220 -2
View File
@@ -15,6 +15,28 @@ function deserialize_messages_ButtonReplyRequest(buffer_arg) {
return gows_pb.ButtonReplyRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_CancelEventMessageRequest(arg) {
if (!(arg instanceof gows_pb.CancelEventMessageRequest)) {
throw new Error('Expected argument of type messages.CancelEventMessageRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_CancelEventMessageRequest(buffer_arg) {
return gows_pb.CancelEventMessageRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_ChatLabelRequest(arg) {
if (!(arg instanceof gows_pb.ChatLabelRequest)) {
throw new Error('Expected argument of type messages.ChatLabelRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_ChatLabelRequest(buffer_arg) {
return gows_pb.ChatLabelRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_ChatPresenceRequest(arg) {
if (!(arg instanceof gows_pb.ChatPresenceRequest)) {
throw new Error('Expected argument of type messages.ChatPresenceRequest');
@@ -70,6 +92,17 @@ function deserialize_messages_CreateNewsletterRequest(buffer_arg) {
return gows_pb.CreateNewsletterRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_DeleteLabelRequest(arg) {
if (!(arg instanceof gows_pb.DeleteLabelRequest)) {
throw new Error('Expected argument of type messages.DeleteLabelRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_DeleteLabelRequest(buffer_arg) {
return gows_pb.DeleteLabelRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_DownloadMediaRequest(arg) {
if (!(arg instanceof gows_pb.DownloadMediaRequest)) {
throw new Error('Expected argument of type messages.DownloadMediaRequest');
@@ -158,6 +191,28 @@ function deserialize_messages_GetContactsRequest(buffer_arg) {
return gows_pb.GetContactsRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_GetLabelsRequest(arg) {
if (!(arg instanceof gows_pb.GetLabelsRequest)) {
throw new Error('Expected argument of type messages.GetLabelsRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_GetLabelsRequest(buffer_arg) {
return gows_pb.GetLabelsRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_GetLidsRequest(arg) {
if (!(arg instanceof gows_pb.GetLidsRequest)) {
throw new Error('Expected argument of type messages.GetLidsRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_GetLidsRequest(buffer_arg) {
return gows_pb.GetLidsRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_GetMessagesRequest(arg) {
if (!(arg instanceof gows_pb.GetMessagesRequest)) {
throw new Error('Expected argument of type messages.GetMessagesRequest');
@@ -389,6 +444,17 @@ function deserialize_messages_OptionalString(buffer_arg) {
return gows_pb.OptionalString.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_OptionalUInt64(arg) {
if (!(arg instanceof gows_pb.OptionalUInt64)) {
throw new Error('Expected argument of type messages.OptionalUInt64');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_OptionalUInt64(buffer_arg) {
return gows_pb.OptionalUInt64.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_PairCodeRequest(arg) {
if (!(arg instanceof gows_pb.PairCodeRequest)) {
throw new Error('Expected argument of type messages.PairCodeRequest');
@@ -576,6 +642,17 @@ function deserialize_messages_UpdateParticipantsRequest(buffer_arg) {
return gows_pb.UpdateParticipantsRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_UpsertLabelRequest(arg) {
if (!(arg instanceof gows_pb.UpsertLabelRequest)) {
throw new Error('Expected argument of type messages.UpsertLabelRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_UpsertLabelRequest(buffer_arg) {
return gows_pb.UpsertLabelRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
//
// Events
@@ -594,7 +671,7 @@ var EventStreamService = exports.EventStreamService = {
},
};
exports.EventStreamClient = grpc.makeGenericClientConstructor(EventStreamService);
exports.EventStreamClient = grpc.makeGenericClientConstructor(EventStreamService, 'EventStream');
var MessageServiceService = exports.MessageServiceService = {
//
// Session management
@@ -691,6 +768,53 @@ setProfileName: {
responseDeserialize: deserialize_messages_Empty,
},
//
// Lids
//
getAllLids: {
path: '/messages.MessageService/GetAllLids',
requestStream: false,
responseStream: false,
requestType: gows_pb.GetLidsRequest,
responseType: gows_pb.JsonList,
requestSerialize: serialize_messages_GetLidsRequest,
requestDeserialize: deserialize_messages_GetLidsRequest,
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
getLidsCount: {
path: '/messages.MessageService/GetLidsCount',
requestStream: false,
responseStream: false,
requestType: gows_pb.Session,
responseType: gows_pb.OptionalUInt64,
requestSerialize: serialize_messages_Session,
requestDeserialize: deserialize_messages_Session,
responseSerialize: serialize_messages_OptionalUInt64,
responseDeserialize: deserialize_messages_OptionalUInt64,
},
findPNByLid: {
path: '/messages.MessageService/FindPNByLid',
requestStream: false,
responseStream: false,
requestType: gows_pb.EntityByIdRequest,
responseType: gows_pb.OptionalString,
requestSerialize: serialize_messages_EntityByIdRequest,
requestDeserialize: deserialize_messages_EntityByIdRequest,
responseSerialize: serialize_messages_OptionalString,
responseDeserialize: deserialize_messages_OptionalString,
},
findLIDByPhoneNumber: {
path: '/messages.MessageService/FindLIDByPhoneNumber',
requestStream: false,
responseStream: false,
requestType: gows_pb.EntityByIdRequest,
responseType: gows_pb.OptionalString,
requestSerialize: serialize_messages_EntityByIdRequest,
requestDeserialize: deserialize_messages_EntityByIdRequest,
responseSerialize: serialize_messages_OptionalString,
responseDeserialize: deserialize_messages_OptionalString,
},
//
// Groups
//
fetchGroups: {
@@ -1090,6 +1214,100 @@ getSubscribedNewsletters: {
responseDeserialize: deserialize_messages_Empty,
},
//
// Labels
//
getLabels: {
path: '/messages.MessageService/GetLabels',
requestStream: false,
responseStream: false,
requestType: gows_pb.GetLabelsRequest,
responseType: gows_pb.JsonList,
requestSerialize: serialize_messages_GetLabelsRequest,
requestDeserialize: deserialize_messages_GetLabelsRequest,
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
upsertLabel: {
path: '/messages.MessageService/UpsertLabel',
requestStream: false,
responseStream: false,
requestType: gows_pb.UpsertLabelRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_UpsertLabelRequest,
requestDeserialize: deserialize_messages_UpsertLabelRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
deleteLabel: {
path: '/messages.MessageService/DeleteLabel',
requestStream: false,
responseStream: false,
requestType: gows_pb.DeleteLabelRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_DeleteLabelRequest,
requestDeserialize: deserialize_messages_DeleteLabelRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
addChatLabel: {
path: '/messages.MessageService/AddChatLabel',
requestStream: false,
responseStream: false,
requestType: gows_pb.ChatLabelRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_ChatLabelRequest,
requestDeserialize: deserialize_messages_ChatLabelRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
removeChatLabel: {
path: '/messages.MessageService/RemoveChatLabel',
requestStream: false,
responseStream: false,
requestType: gows_pb.ChatLabelRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_ChatLabelRequest,
requestDeserialize: deserialize_messages_ChatLabelRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
getLabelsByJid: {
path: '/messages.MessageService/GetLabelsByJid',
requestStream: false,
responseStream: false,
requestType: gows_pb.EntityByIdRequest,
responseType: gows_pb.JsonList,
requestSerialize: serialize_messages_EntityByIdRequest,
requestDeserialize: deserialize_messages_EntityByIdRequest,
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
getChatsByLabelId: {
path: '/messages.MessageService/GetChatsByLabelId',
requestStream: false,
responseStream: false,
requestType: gows_pb.EntityByIdRequest,
responseType: gows_pb.JsonList,
requestSerialize: serialize_messages_EntityByIdRequest,
requestDeserialize: deserialize_messages_EntityByIdRequest,
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
//
// Events
//
cancelEventMessage: {
path: '/messages.MessageService/CancelEventMessage',
requestStream: false,
responseStream: false,
requestType: gows_pb.CancelEventMessageRequest,
responseType: gows_pb.MessageResponse,
requestSerialize: serialize_messages_CancelEventMessageRequest,
requestDeserialize: deserialize_messages_CancelEventMessageRequest,
responseSerialize: serialize_messages_MessageResponse,
responseDeserialize: deserialize_messages_MessageResponse,
},
//
// Media
//
downloadMedia: {
@@ -1163,4 +1381,4 @@ getMessageById: {
},
};
exports.MessageServiceClient = grpc.makeGenericClientConstructor(MessageServiceService);
exports.MessageServiceClient = grpc.makeGenericClientConstructor(MessageServiceService, 'MessageService');
File diff suppressed because it is too large. Load diff
+45
View File
@@ -0,0 +1,45 @@
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
import { Label, LabelChatAssociation } from '@waha/structures/labels.dto';
import * as gows from './types';
/**
* Converts a GOWS LabelEdit event to a Label DTO
*/
export function eventToLabelDTO(labelEdit: gows.LabelEdit): Label {
return {
id: labelEdit.LabelID,
name: labelEdit.Action.name,
color: labelEdit.Action.color,
colorHex: Label.toHex(labelEdit.Action.color),
} as Label;
}
/**
* Converts a GOWS LabelAssociationChat event to a LabelChatAssociation DTO
*/
export function eventToLabelChatAssociationDTO(
labelAssoc: gows.LabelAssociationChat,
): LabelChatAssociation {
return {
labelId: labelAssoc.LabelID,
label: null, // We don't have the label info here
chatId: toCusFormat(labelAssoc.JID),
};
}
/**
* Checks if a LabelEdit event is for a label creation or update (not deletion)
*/
export function isLabelUpsertEvent(labelEdit: gows.LabelEdit): boolean {
return !labelEdit.Action.deleted;
}
/**
* Checks if a LabelAssociationChat event is for adding a label to a chat
*/
export function isLabelChatAddedEvent(
labelAssoc: gows.LabelAssociationChat,
): boolean {
return labelAssoc.Action.labeled === true;
}
+458 -11
View File
@@ -5,6 +5,7 @@ import {
isJidGroup,
jidNormalizedUser,
normalizeMessageContent,
WAMessageKey,
} from '@adiwajshing/baileys';
import { isJidBroadcast } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import * as grpc from '@grpc/grpc-js';
@@ -32,7 +33,10 @@ import {
statusToAck,
} from '@waha/core/engines/gows/helpers';
import { GowsAuthFactoryCore } from '@waha/core/engines/gows/store/GowsAuthFactoryCore';
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
import {
getDestination,
toCusFormat,
} from '@waha/core/engines/noweb/session.noweb.core';
import { extractMediaContent } from '@waha/core/engines/noweb/utils';
import {
AvailableInPlusVersion,
@@ -60,6 +64,7 @@ import {
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
OverviewFilter,
ReadChatMessagesQuery,
ReadChatMessagesResponse,
} from '@waha/structures/chats.dto';
@@ -67,11 +72,13 @@ import {
ChatRequest,
CheckNumberStatusQuery,
EditMessageRequest,
MessageContactVcardRequest,
MessageFileRequest,
MessageForwardRequest,
MessageImageRequest,
MessageLinkCustomPreviewRequest,
MessageLocationRequest,
MessagePollRequest,
MessageReactionRequest,
MessageReplyRequest,
MessageTextRequest,
@@ -87,6 +94,12 @@ import {
WAHASessionStatus,
WAMessageAck,
} from '@waha/structures/enums.dto';
import {
EventCancelRequest,
EventMessageRequest,
EventResponse,
EventResponsePayload,
} from '@waha/structures/events.dto';
import { BinaryFile, RemoteFile } from '@waha/structures/files.dto';
import {
CreateGroupRequest,
@@ -112,7 +125,11 @@ import {
DeleteStatusRequest,
TextStatus,
} from '@waha/structures/status.dto';
import { EnginePayload, WAMessageAckBody } from '@waha/structures/webhooks.dto';
import {
EnginePayload,
PollVotePayload,
WAMessageAckBody,
} from '@waha/structures/webhooks.dto';
import { PaginatorInMemory } from '@waha/utils/Paginator';
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
import { onlyEvent } from '@waha/utils/reactive/ops/onlyEvent';
@@ -132,8 +149,22 @@ import { promisify } from 'util';
import * as gows from './types';
import { MessageStatus } from './types';
import MessageServiceClient = messages.MessageServiceClient;
import { isFromFullSync } from '@waha/core/engines/gows/appstate';
import { toVcard } from '@waha/core/helpers';
import { AckToStatus } from '@waha/core/utils/acks';
import { ParseEventResponseType } from '@waha/core/utils/events';
import { DistinctAck } from '@waha/core/utils/reactive';
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import { exclude } from '@waha/utils/reactive/ops/exclude';
import * as lodash from 'lodash';
import {
eventToLabelChatAssociationDTO,
eventToLabelDTO,
isLabelChatAddedEvent,
isLabelUpsertEvent,
} from './labels.gows';
enum WhatsMeowEvent {
CONNECTED = 'gows.ConnectedEventData',
@@ -150,6 +181,13 @@ enum WhatsMeowEvent {
// Groups
GROUP_INFO = 'events.GroupInfo',
JOINED_GROUP = 'events.JoinedGroup',
// Labels
LABEL_EDIT = 'events.LabelEdit',
LABEL_ASSOCIATION_CHAT = 'events.LabelAssociationChat',
// Events
EVENT_MESSAGE_RESPONSE = 'gows.EventMessageResponse',
// Polls
POLL_VOTE_EVENT = 'gows.PollVoteEvent',
}
const gRPCClientConfig = {
@@ -419,6 +457,92 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
filter(Boolean),
);
this.events2.get(WAHAEvents.GROUP_V2_UPDATE).switch(groupV2Update$);
// Label Events
// First, create streams for the raw label events
const labelEditEvents$ = all$.pipe(
onlyEvent(WhatsMeowEvent.LABEL_EDIT),
exclude(isFromFullSync),
);
// Split the raw label edit events into upsert and deleted events
const [labelUpsertEvents$, labelDeletedEvents$] = partition(
labelEditEvents$,
isLabelUpsertEvent,
);
// Convert the events to DTOs
const labelUpsert$ = labelUpsertEvents$.pipe(map(eventToLabelDTO));
const labelDeleted$ = labelDeletedEvents$.pipe(map(eventToLabelDTO));
this.events2.get(WAHAEvents.LABEL_UPSERT).switch(labelUpsert$);
this.events2.get(WAHAEvents.LABEL_DELETED).switch(labelDeleted$);
//
// Polls
//
const pollVoteEvent$ = all$.pipe(
onlyEvent(WhatsMeowEvent.POLL_VOTE_EVENT),
map(this.toPollVotePayload.bind(this)),
filter(Boolean),
);
// Split into successful and failed responses
const [pollVoteSuccess$, pollVoteFailed$] = partition(
pollVoteEvent$,
(payload: PollVotePayload) => !!payload.vote.selectedOptions,
);
this.events2.get(WAHAEvents.POLL_VOTE).switch(pollVoteSuccess$);
this.events2.get(WAHAEvents.POLL_VOTE_FAILED).switch(pollVoteFailed$);
//
// Event Message
//
const eventMessageResponse$ = all$.pipe(
onlyEvent(WhatsMeowEvent.EVENT_MESSAGE_RESPONSE),
map(this.toEventResponsePayload.bind(this)),
filter(Boolean),
);
// Split into successful and failed responses
const [eventResponseSuccess$, eventResponseFailed$] = partition(
eventMessageResponse$,
(payload: EventResponsePayload) => !!payload.eventResponse,
);
this.events2.get(WAHAEvents.EVENT_RESPONSE).switch(eventResponseSuccess$);
this.events2
.get(WAHAEvents.EVENT_RESPONSE_FAILED)
.switch(eventResponseFailed$);
//
// Labels
//
// Handle label association events
const labelAssociationEvents$ = all$.pipe(
onlyEvent(WhatsMeowEvent.LABEL_ASSOCIATION_CHAT),
exclude(isFromFullSync),
);
// Split the raw label association events into added and deleted events
const [labelChatAddedEvents$, labelChatDeletedEvents$] = partition(
labelAssociationEvents$,
isLabelChatAddedEvent,
);
// Convert the events to DTOs
const labelChatAdded$ = labelChatAddedEvents$.pipe(
map(eventToLabelChatAssociationDTO),
);
const labelChatDeleted$ = labelChatDeletedEvents$.pipe(
map(eventToLabelChatAssociationDTO),
);
this.events2.get(WAHAEvents.LABEL_CHAT_ADDED).switch(labelChatAdded$);
this.events2.get(WAHAEvents.LABEL_CHAT_DELETED).switch(labelChatDeleted$);
}
async fetchContactProfilePicture(id: string): Promise<string> {
@@ -587,6 +711,37 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
return this.messageResponse(jid, data);
}
async sendContactVCard(request: MessageContactVcardRequest) {
const jid = toJID(this.ensureSuffix(request.chatId));
const contacts = request.contacts.map((el) => ({ vcard: toVcard(el) }));
const message = new messages.MessageRequest({
jid: jid,
session: this.session,
replyTo: getMessageIdFromSerialized(request.reply_to),
contacts: contacts.map((contact) => new messages.vCardContact(contact)),
});
const response = await promisify(this.client.SendMessage)(message);
const data = response.toObject();
return this.messageResponse(jid, data);
}
async sendPoll(request: MessagePollRequest) {
const jid = toJID(request.chatId);
const message = new messages.MessageRequest({
jid: jid,
session: this.session,
replyTo: getMessageIdFromSerialized(request.reply_to),
poll: new messages.PollMessage({
name: request.poll.name,
options: request.poll.options,
multipleAnswers: request.poll.multipleAnswers,
}),
});
const response = await promisify(this.client.SendMessage)(message);
const data = response.toObject();
return this.messageResponse(jid, data);
}
public async deleteMessage(chatId: string, messageId: string) {
const jid = toJID(this.ensureSuffix(chatId));
const key = parseMessageIdSerialized(messageId);
@@ -946,6 +1101,59 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
return this.messageResponse(key.remoteJid, data);
}
async sendEvent(request: EventMessageRequest): Promise<WAMessage> {
const jid = toJID(this.ensureSuffix(request.chatId));
const event = request.event;
// Create EventLocation if provided
let location = null;
if (event.location) {
location = new messages.EventLocation({
name: event.location.name,
// Doesn't work right now
degreesLatitude: 0,
degreesLongitude: 0,
});
}
// Create event payload
const eventMessage = new messages.EventMessage({
name: event.name,
description: event.description,
startTime: event.startTime,
endTime: event.endTime,
location: location,
extraGuestsAllowed: event.extraGuestsAllowed,
});
// Create message
const message = new messages.MessageRequest({
jid: jid,
session: this.session,
event: eventMessage,
replyTo: getMessageIdFromSerialized(request.reply_to),
});
const response = await promisify(this.client.SendMessage)(message);
const data = response.toObject();
return this.messageResponse(jid, data) as any;
}
async cancelEvent(eventId: string): Promise<WAMessage> {
throw new Error('Method not implemented.');
const key = parseMessageIdSerialized(eventId, false);
const jid = key.remoteJid;
const request = new messages.CancelEventMessageRequest({
session: this.session,
jid: jid,
messageId: key.id,
});
const response = await promisify(this.client.CancelEventMessage)(request);
const data = response.toObject();
return this.messageResponse(jid, data) as any;
}
public async setPresence(presence: WAHAPresenceStatus, chatId?: string) {
let request: any;
let method: any;
@@ -1006,13 +1214,13 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
}
public async getPresence(chatId: string): Promise<WAHAChatPresences> {
const remoteJid = toJID(chatId);
if (!(remoteJid in this.presences.keys())) {
await this.subscribePresence(remoteJid);
const jid = toJID(chatId);
await this.subscribePresence(jid);
if (!(jid in this.presences.keys())) {
await sleep(1000);
}
const result = this.presences.get(remoteJid) || [];
return this.toWahaPresences(remoteJid, result);
const result = this.presences.get(jid) || [];
return this.toWahaPresences(jid, result);
}
async subscribePresence(chatId: string) {
@@ -1246,11 +1454,68 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
return data.map(this.toWAContact.bind(this));
}
/**
* Lid to Phone Number methods
*/
public async getAllLids(
pagination: PaginationParams,
): Promise<Array<LidToPhoneNumber>> {
const request = new messages.GetLidsRequest({
session: this.session,
});
const response = await promisify(this.client.GetAllLids)(request);
const data = parseJsonList(response);
const lids = data.map((item) => ({
lid: item.lid,
pn: toCusFormat(item.pn),
}));
// Use in-memory pagination
const paginator = new PaginatorInMemory(pagination);
return paginator.apply(lids);
}
public async getLidsCount(): Promise<number> {
const response = await promisify(this.client.GetLidsCount)(this.session);
return response?.value;
}
public async findPNByLid(lid: string): Promise<LidToPhoneNumber> {
const request = new messages.EntityByIdRequest({
session: this.session,
id: lid,
});
const response = await promisify(this.client.FindPNByLid)(request);
const phoneNumber = response?.value;
return {
lid: lid,
pn: phoneNumber ? toCusFormat(phoneNumber) : null,
};
}
public async findLIDByPhoneNumber(
phoneNumber: string,
): Promise<LidToPhoneNumber> {
const pn = toJID(phoneNumber);
const request = new messages.EntityByIdRequest({
session: this.session,
id: pn,
});
const response = await promisify(this.client.FindLIDByPhoneNumber)(request);
const lid = response.value;
return {
lid: lid || null,
pn: toCusFormat(pn),
};
}
/**
* Chats methods
*/
public async getChatsOverview(
pagination: PaginationParams,
filter?: OverviewFilter,
): Promise<ChatSummary[]> {
if (!pagination.sortBy) {
pagination.sortBy = 'timestamp';
@@ -1258,7 +1523,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
if (!pagination.sortOrder) {
pagination.sortOrder = SortOrder.DESC;
}
const chats = await this.getChats(pagination);
const chats = await this.getChats(pagination, filter);
const promises = [];
for (const chat of chats) {
@@ -1297,10 +1562,17 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
};
}
public async getChats(pagination: PaginationParams) {
public async getChats(
pagination: PaginationParams,
filter: OverviewFilter | null = null,
) {
if (pagination.sortBy === ChatSortField.CONVERSATION_TIMESTAMP) {
pagination.sortBy = 'timestamp';
}
let jids = [];
if (filter?.ids && filter.ids.length > 0) {
jids = filter.ids.map((id) => toJID(id));
}
const request = new messages.GetChatsRequest({
session: this.session,
pagination: new messages.Pagination({
@@ -1314,6 +1586,9 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
? messages.SortBy.Order.DESC
: messages.SortBy.Order.ASC,
}),
filter: new messages.ChatFilter({
jids: jids,
}),
});
const response = await promisify(this.client.GetChats)(request);
const data = parseJsonList(response);
@@ -1390,6 +1665,119 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
return this.processIncomingMessage(msg, query.downloadMedia);
}
/**
* Labels methods
*/
public async getLabels(): Promise<Label[]> {
const request = new messages.GetLabelsRequest({
session: this.session,
});
const response = await promisify(this.client.GetLabels)(request);
const labels = parseJsonList(response);
return labels.map(this.toLabel);
}
public async createLabel(labelDto: LabelDTO): Promise<Label> {
const labels = await this.getLabels();
const highestLabelId = lodash.max(
labels.map((label) => parseInt(label.id)),
);
const labelId = highestLabelId ? highestLabelId + 1 : 1;
const label: Label = {
id: labelId.toString(),
name: labelDto.name,
color: labelDto.color,
colorHex: Label.toHex(labelDto.color),
};
return this.updateLabel(label);
}
protected toLabel(label: any): Label {
const color = label.color;
return {
id: label.id,
name: label.name,
color: color,
colorHex: Label.toHex(color),
};
}
public async updateLabel(label: Label): Promise<Label> {
const request = new messages.UpsertLabelRequest({
session: this.session,
label: new messages.Label({
id: label.id,
name: label.name,
color: label.color,
}),
});
await promisify(this.client.UpsertLabel)(request);
return label;
}
public async deleteLabel(label: Label): Promise<void> {
const request = new messages.DeleteLabelRequest({
session: this.session,
label: new messages.Label({
id: label.id,
name: label.name,
color: label.color,
}),
});
await promisify(this.client.DeleteLabel)(request);
}
public async getChatsByLabelId(labelId: string) {
const request = new messages.EntityByIdRequest({
session: this.session,
id: labelId,
});
const response = await promisify(this.client.GetChatsByLabelId)(request);
const ids = parseJsonList(response);
return ids.map((jid) => {
return {
id: toCusFormat(jid),
};
});
}
public async getChatLabels(chatId: string): Promise<Label[]> {
const jid = toJID(chatId);
const request = new messages.EntityByIdRequest({
session: this.session,
id: jid,
});
const response = await promisify(this.client.GetLabelsByJid)(request);
const labels = parseJsonList(response);
return labels.map(this.toLabel);
}
public async putLabelsToChat(chatId: string, labels: LabelID[]) {
const jid = toJID(chatId);
const labelsIds = labels.map((label) => label.id);
const currentLabels = await this.getChatLabels(jid);
const currentLabelsIds = currentLabels.map((label) => label.id);
const addLabelsIds = lodash.difference(labelsIds, currentLabelsIds);
const removeLabelsIds = lodash.difference(currentLabelsIds, labelsIds);
for (const labelId of addLabelsIds) {
const request = new messages.ChatLabelRequest({
session: this.session,
labelId: labelId,
chatId: jid,
});
await promisify(this.client.AddChatLabel)(request);
}
for (const labelId of removeLabelsIds) {
const request = new messages.ChatLabelRequest({
session: this.session,
labelId: labelId,
chatId: jid,
});
await promisify(this.client.RemoveChatLabel)(request);
}
}
//
// END - Methods for API
//
@@ -1398,10 +1786,12 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
// if there is no text or media message
if (!message) return;
if (!message.Message) return;
// Ignore reactions, we have dedicated handler for that
// Ignore reactions, we have a dedicated handler for that
if (message.Message.reactionMessage) return;
// Ignore poll votes, we have dedicated handler for that
// Ignore poll votes, we have a dedicated handler for that
if (message.Message.pollUpdateMessage) return;
// Ignore event response, we have a dedicated handler for that
if (message.Message.encEventResponseMessage) return;
// Ignore protocol messages
if (message.Message.protocolMessage) return;
@@ -1464,6 +1854,63 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
};
}
private toPollVotePayload(event: any): PollVotePayload {
// Extract event creation message key from the message
const creationKey = event.Message?.pollUpdateMessage.pollCreationMessageKey;
const key: WAMessageKey = {
remoteJid: creationKey.remoteJID,
fromMe: creationKey.fromMe,
id: creationKey.ID,
participant: creationKey.participant,
};
const fromToParticipant = getFromToParticipant(event);
const pollCreationKey = getDestination(key);
return {
poll: pollCreationKey,
vote: {
id: buildMessageId(event),
from: toCusFormat(fromToParticipant.from),
fromMe: event.Info.IsFromMe,
to: toCusFormat(fromToParticipant.to),
participant: toCusFormat(fromToParticipant.participant),
selectedOptions: event.Votes,
timestamp: event.Message.pollUpdateMessage.senderTimestampMS,
},
_data: event,
};
}
private toEventResponsePayload(event: any): EventResponsePayload {
const msg = this.toWAMessage(event);
let response: EventResponse | null = null;
if (event.EventResponse) {
response = {
response: ParseEventResponseType(event.EventResponse.response),
timestampMs: event.EventResponse.timestampMS,
extraGuestCount: event.EventResponse.extraGuestCount || 0,
};
}
// Extract event creation message key from the message
const message = event.Message || event.message;
const eventCreationMessageKey =
message?.encEventResponseMessage?.eventCreationMessageKey;
const key: WAMessageKey = {
remoteJid: eventCreationMessageKey.remoteJID,
fromMe: eventCreationMessageKey.fromMe,
id: eventCreationMessageKey.ID,
participant: eventCreationMessageKey.participant,
};
const eventCreationKey = getDestination(key);
return {
...msg,
eventCreationKey: eventCreationKey,
eventResponse: response,
_data: event,
};
}
private getSourceDeviceByMsg(message): MessageSource {
if (!message.Info.IsFromMe) {
return MessageSource.APP;
+28
View File
@@ -39,3 +39,31 @@ export enum MessageStatus {
Read = 4,
Played = 5,
}
export interface LabelEditAction {
name: string;
color: number;
deleted: boolean;
orderIndex: number;
isActive: boolean;
type: number;
}
export interface LabelEdit {
Timestamp: string;
LabelID: string;
Action: LabelEditAction;
FromFullSync: boolean;
}
export interface LabelAssociationChatAction {
labeled: boolean;
}
export interface LabelAssociationChat {
JID: string;
Timestamp: string;
LabelID: string;
Action: LabelAssociationChatAction;
FromFullSync: boolean;
}
+63 -11
View File
@@ -90,6 +90,7 @@ import {
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
OverviewFilter,
PinDuration,
ReadChatMessagesQuery,
ReadChatMessagesResponse,
@@ -138,6 +139,7 @@ import {
LabelDTO,
LabelID,
} from '@waha/structures/labels.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import { ReplyToMessage } from '@waha/structures/message.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import {
@@ -385,7 +387,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
resubscribeToKnownPresences() {
for (const jid in this.store.presences) {
this.sock.presenceSubscribe(jid);
this.subscribePresence(jid);
}
}
@@ -467,7 +469,10 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
} else if (connection === 'open') {
this.qr.save('');
this.status = WAHASessionStatus.WORKING;
this.resubscribeToKnownPresences();
// Do we need to resubscribe?
// Ideally not, we need to explicitly call interesting
// jids every 1 minute
// this.resubscribeToKnownPresences();
return;
} else if (connection === 'close') {
this.qr.save('');
@@ -615,7 +620,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
}
for (const message of messages) {
const content = normalizeMessageContent(message.message);
if (!content.pollUpdateMessage) {
if (!content?.pollUpdateMessage) {
continue;
}
const creationMsgKey = content.pollUpdateMessage.pollCreationMessageKey;
@@ -1126,8 +1131,17 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
public async getChatsOverview(
pagination: PaginationParams,
filter?: OverviewFilter,
): Promise<ChatSummary[]> {
const chats = await this.store.getChats(pagination, false);
// Convert customer format IDs to JID format if filter is provided
let jidFilter;
if (filter?.ids && filter.ids.length > 0) {
jidFilter = {
ids: filter.ids.map((id) => toJID(id)),
};
}
const chats = await this.store.getChats(pagination, false, jidFilter);
// Remove unreadCount, it's not ready yet
chats.forEach((chat) => delete chat.unreadCount);
@@ -1339,6 +1353,44 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
throw new NotImplementedByEngineError();
}
/**
* Lid to Phone Number methods
*/
public async getAllLids(
pagination: PaginationParams,
): Promise<Array<LidToPhoneNumber>> {
const lids = await this.store.getAllLids(pagination);
return lids.map((value) => {
return {
lid: value.lid,
pn: toCusFormat(value.pn),
};
});
}
public async getLidsCount(): Promise<number> {
return this.store.getLidsCount();
}
public async findPNByLid(lid: string): Promise<LidToPhoneNumber> {
const pn = await this.store.findPNByLid(lid);
return {
lid: lid,
pn: pn ? toCusFormat(pn) : null,
};
}
public async findLIDByPhoneNumber(
phoneNumber: string,
): Promise<LidToPhoneNumber> {
const pn = toJID(phoneNumber);
const lid = await this.store.findLidByPN(pn);
return {
lid: lid || null,
pn: toCusFormat(pn),
};
}
/**
* Group methods
*/
@@ -1473,14 +1525,14 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
}
public async getPresence(chatId: string): Promise<WAHAChatPresences> {
const remoteJid = toJID(chatId);
if (!(remoteJid in this.store.presences)) {
this.store.presences[remoteJid] = {};
await this.sock.presenceSubscribe(remoteJid);
const jid = toJID(chatId);
await this.subscribePresence(jid);
if (!(jid in this.store.presences)) {
this.store.presences[jid] = {};
await sleep(1000);
}
const result = this.store.presences[remoteJid];
return this.toWahaPresences(remoteJid, result);
const result = this.store.presences[jid];
return this.toWahaPresences(jid, result);
}
public subscribePresence(id: string): Promise<void> {
@@ -2561,7 +2613,7 @@ function getFrom(key, meId) {
return key.remoteJid;
}
function getDestination(key, meId = undefined): MessageDestination {
export function getDestination(key, meId = undefined): MessageDestination {
return {
id: buildMessageId(key),
to: toCusFormat(getTo(key, meId)),
@@ -1,4 +1,5 @@
import { Chat } from '@adiwajshing/baileys';
import { OverviewFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
export interface IChatRepository {
@@ -9,6 +10,7 @@ export interface IChatRepository {
getAllWithMessages(
pagination: PaginationParams,
broadcast: boolean,
filter?: OverviewFilter,
): Promise<Chat[]>;
getById(id: string): Promise<Chat | null>;
@@ -0,0 +1,33 @@
import { LimitOffsetParams } from '@waha/structures/pagination.dto';
export class LidToPN {
id: string;
pn: string;
}
export interface INowebLidPNRepository {
/**
* Save lid to phone number mapping
*/
saveLids(lids: LidToPN[]): Promise<void>;
/**
* Get all lids with pagination
*/
getAllLids(pagination?: LimitOffsetParams): Promise<LidToPN[]>;
/**
* Get a total count of lids
*/
getLidsCount(): Promise<number>;
/**
* Find a phone number by lid
*/
findPNByLid(lid: string): Promise<string | null>;
/**
* Find lid by phone number
*/
findLidByPN(pn: string): Promise<string | null>;
}
@@ -5,6 +5,7 @@ import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsReposit
import { IChatRepository } from './IChatRepository';
import { IContactRepository } from './IContactRepository';
import { IMessagesRepository } from './IMessagesRepository';
import { INowebLidPNRepository } from './INowebLidPNRepository';
export abstract class INowebStorage {
abstract init(): Promise<void>;
@@ -22,4 +23,6 @@ export abstract class INowebStorage {
abstract getLabelsRepository(): ILabelsRepository;
abstract getLabelAssociationRepository(): ILabelAssociationRepository;
abstract getLidPNRepository(): INowebLidPNRepository;
}
+23 -3
View File
@@ -6,8 +6,15 @@ import {
} from '@adiwajshing/baileys';
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import {
GetChatMessagesFilter,
OverviewFilter,
} from '@waha/structures/chats.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import {
LimitOffsetParams,
PaginationParams,
} from '@waha/structures/pagination.dto';
export interface INowebStore {
presences: any;
@@ -28,7 +35,11 @@ export interface INowebStore {
getMessageById(chatId: string, messageId: string): Promise<any>;
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]>;
getChats(
pagination: PaginationParams,
broadcast: boolean,
filter?: OverviewFilter,
): Promise<Chat[]>;
getChat(jid: string): Promise<Chat | null>;
@@ -47,4 +58,13 @@ export interface INowebStore {
getGroups(pagination: PaginationParams): Promise<GroupMetadata[]>;
resetGroupsCache(): void;
// Lid Repository methods
getAllLids(pagination?: LimitOffsetParams): Promise<LidToPhoneNumber[]>;
getLidsCount(): Promise<number>;
findPNByLid(lid: string): Promise<string | null>;
findLidByPN(pn: string): Promise<string | null>;
}
@@ -7,7 +7,11 @@ import makeWASocket, {
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import { BadRequestException } from '@nestjs/common';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import {
LimitOffsetParams,
PaginationParams,
} from '@waha/structures/pagination.dto';
import { PaginatorInMemory } from '@waha/utils/Paginator';
import { INowebStore } from './INowebStore';
@@ -107,4 +111,23 @@ export class NowebInMemoryStore implements INowebStore {
resetGroupsCache() {
return;
}
//
// Lids methods
//
getAllLids(pagination?: LimitOffsetParams): Promise<LidToPhoneNumber[]> {
throw new BadRequestException(this.errorMessage);
}
findLidByPN(pn: string): Promise<string | null> {
throw new BadRequestException(this.errorMessage);
}
findPNByLid(lid: string): Promise<string | null> {
throw new BadRequestException(this.errorMessage);
}
getLidsCount(): Promise<number> {
throw new BadRequestException(this.errorMessage);
}
}
@@ -5,6 +5,7 @@ import makeWASocket, {
ChatUpdate,
Contact,
GroupParticipant,
isJidUser,
isRealMessage,
jidNormalizedUser,
ParticipantAction,
@@ -18,13 +19,22 @@ import {
LabelAssociation,
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
import {
GetChatMessagesFilter,
OverviewFilter,
} from '@waha/structures/chats.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import {
LimitOffsetParams,
PaginationParams,
SortOrder,
} from '@waha/structures/pagination.dto';
import { DefaultMap } from '@waha/utils/DefaultMap';
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
import { waitUntil } from '@waha/utils/promiseTimeout';
import * as lodash from 'lodash';
import { toNumber } from 'lodash';
import { Logger } from 'pino';
@@ -32,6 +42,7 @@ import { Logger } from 'pino';
import { IChatRepository } from './IChatRepository';
import { IContactRepository } from './IContactRepository';
import { IMessagesRepository } from './IMessagesRepository';
import { INowebLidPNRepository, LidToPN } from './INowebLidPNRepository';
import { INowebStorage } from './INowebStorage';
import { INowebStore } from './INowebStore';
@@ -49,8 +60,14 @@ export class NowebPersistentStore implements INowebStore {
private messagesRepo: IMessagesRepository;
private labelsRepo: ILabelsRepository;
private labelAssociationsRepo: ILabelAssociationRepository;
private lidRepo: INowebLidPNRepository;
public presences: any;
private lock: any;
private lock: any = new AsyncLock({
maxPending: Infinity,
maxExecutionTime: 60_000,
});
private groupsFetchLock: any = new AsyncLock({
maxPending: Infinity,
maxExecutionTime: 60_000,
@@ -71,8 +88,8 @@ export class NowebPersistentStore implements INowebStore {
this.messagesRepo = storage.getMessagesRepository();
this.labelsRepo = storage.getLabelsRepository();
this.labelAssociationsRepo = storage.getLabelAssociationRepository();
this.lidRepo = storage.getLidPNRepository();
this.presences = {};
this.lock = new AsyncLock({ maxPending: Infinity });
}
init(): Promise<void> {
@@ -112,9 +129,16 @@ export class NowebPersistentStore implements INowebStore {
ev.on('groups.upsert', (data) =>
this.withLock('groups', () => this.onGroupUpsert(data)),
);
ev.on('groups.update', (data) =>
this.withLock('groups', () => this.onGroupUpdate(data)),
);
ev.on('groups.update', (data) => {
this.withLock('groups', () => this.onGroupUpdate(data));
this.withLock('lids', async () => {
const participants = lodash.flatMap(data, (g) => g?.participants || []);
const lids = await this.handleLidPNUpdates(participants);
this.logger.debug(
`groups.update - '${lids.length}' synced lid to pn mapping`,
);
});
});
ev.on('group-participants.update', (data) =>
this.withLock(`group-${data.id}`, () =>
this.onGroupParticipantsUpdate(data),
@@ -122,12 +146,24 @@ export class NowebPersistentStore implements INowebStore {
);
// Contacts
ev.on('contacts.upsert', (data) =>
this.withLock('contacts', () => this.onContactsUpsert(data)),
);
ev.on('contacts.update', (data) =>
this.withLock('contacts', () => this.onContactUpdate(data)),
);
ev.on('contacts.upsert', (data) => {
this.withLock('contacts', () => this.onContactsUpsert(data));
this.withLock('lids', async () => {
const lids = await this.handleLidPNUpdates(data);
this.logger.debug(
`contacts.upsert - '${lids.length}' synced lid to pn mapping`,
);
});
});
ev.on('contacts.update', (data) => {
this.withLock('contacts', () => this.onContactUpdate(data));
this.withLock('lids', async () => {
const lids = await this.handleLidPNUpdates(data);
this.logger.debug(
`contacts.update - '${lids.length}' synced lid to pn mapping`,
);
});
});
ev.on('labels.edit', (data) => this.onLabelsEdit(data));
ev.on('labels.association', ({ association, type }) =>
this.onLabelsAssociation(association, type),
@@ -162,6 +198,12 @@ export class NowebPersistentStore implements INowebStore {
await this.onContactsUpsert(contacts);
this.logger.info(`history sync - '${contacts.length}' synced contacts`);
}),
this.withLock('lids', async () => {
const lids = await this.handleLidPNUpdates(contacts);
this.logger.info(
`history sync - '${lids.length}' synced lid to pn mapping`,
);
}),
this.withLock('chats', () => this.onChatUpsert(chats)),
this.withLock('messages', () => this.syncMessagesHistory(messages)),
]);
@@ -192,6 +234,9 @@ export class NowebPersistentStore implements INowebStore {
for (const update of updates) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const jid = jidNormalizedUser(update.key.remoteJid!);
if (!update.key.id) {
continue;
}
const message = await this.messagesRepo.getByJidById(jid, update.key.id);
if (!message) {
this.logger.warn(
@@ -482,10 +527,14 @@ export class NowebPersistentStore implements INowebStore {
return this.messagesRepo.getByJidById(chatId, messageId);
}
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]> {
getChats(
pagination: PaginationParams,
broadcast: boolean,
filter?: OverviewFilter,
): Promise<Chat[]> {
pagination.sortBy ||= 'conversationTimestamp';
pagination.sortOrder ||= SortOrder.DESC;
return this.chatRepo.getAllWithMessages(pagination, broadcast);
return this.chatRepo.getAllWithMessages(pagination, broadcast, filter);
}
async getChat(jid: string): Promise<Chat | null> {
@@ -559,4 +608,64 @@ export class NowebPersistentStore implements INowebStore {
const ids = associations.map((association) => association.labelId);
return await this.labelsRepo.getAllByIds(ids);
}
//
// Lid methods
//
private async handleLidPNUpdates(contacts: Array<Partial<Contact>>) {
let lids: LidToPN[] = [];
for (const contact of contacts) {
// contact.id = pn, contact.lid = lid
if (isJidUser(contact.id) && isLidUser(contact.lid)) {
lids.push({
pn: contact.id,
id: contact.lid,
});
}
// contact.pn = pn, contact.lid = lid
else if (isJidUser(contact.pn) && isLidUser(contact.lid)) {
lids.push({
pn: contact.pn,
id: contact.lid,
});
}
// contact.pn = pn, contact.id = lid
else if (isJidUser(contact.pn) && isLidUser(contact.id)) {
lids.push({
pn: contact.pn,
id: contact.id,
});
}
}
// make lids unique by id
lids = lodash.uniqBy(lids, 'id');
if (lids.length > 0) {
await this.lidRepo.saveLids(lids);
}
return lids;
}
async getAllLids(
pagination?: LimitOffsetParams,
): Promise<LidToPhoneNumber[]> {
const lids = await this.lidRepo.getAllLids(pagination);
return lids.map((value) => {
return {
lid: value.id,
pn: value.pn,
};
});
}
getLidsCount(): Promise<number> {
return this.lidRepo.getLidsCount();
}
findPNByLid(lid: string): Promise<string | null> {
return this.lidRepo.findPNByLid(lid);
}
findLidByPN(pn: string): Promise<string | null> {
return this.lidRepo.findLidByPN(pn);
}
}
+14
View File
@@ -65,6 +65,15 @@ export const NowebLabelAssociationsSchema = new Schema(
],
);
export const NowebLidMapSchema = new Schema(
'lid_map',
[new Field('id', 'TEXT'), new Field('pn', 'TEXT'), new Field('data', 'TEXT')],
[
new Index('lid_map_id_index', ['id']),
new Index('lid_map_pn_index', ['pn']),
],
);
export const NOWEB_STORE_SCHEMA = [
NowebContactSchema,
NowebChatSchema,
@@ -72,6 +81,7 @@ export const NOWEB_STORE_SCHEMA = [
NowebMessagesSchema,
NowebLabelsSchema,
NowebLabelAssociationsSchema,
NowebLidMapSchema,
];
export const Migrations: string[] = [
@@ -100,4 +110,8 @@ export const Migrations: string[] = [
'CREATE INDEX IF NOT EXISTS label_assoc_type_label_index ON "labelAssociations" (type, "labelId")',
'CREATE INDEX IF NOT EXISTS label_assoc_type_chat_index ON "labelAssociations" (type, "chatId")',
'CREATE INDEX IF NOT EXISTS label_assoc_type_message_index ON "labelAssociations" (type, "messageId")',
// Lid Map
'CREATE TABLE IF NOT EXISTS lid_map (id TEXT PRIMARY KEY, pn TEXT, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS lid_map_id_index ON lid_map (id)',
'CREATE INDEX IF NOT EXISTS lid_map_pn_index ON lid_map (pn)',
];
@@ -1,5 +1,6 @@
import { Chat } from '@adiwajshing/baileys';
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
import { OverviewFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
export class SqlChatMethods {
@@ -8,15 +9,23 @@ export class SqlChatMethods {
async getAllWithMessages(
pagination: PaginationParams,
broadcast: boolean,
filter?: OverviewFilter,
): Promise<Chat[]> {
// Get chats with conversationTimestamp is not Null
let query = this.repository.select().whereNotNull('conversationTimestamp');
if (!broadcast) {
// filter out chat by id if it ends at @newsletter or @broadcast
query = query
.andWhereNot('id', 'like', '%@broadcast')
.andWhereNot('id', 'like', '%@newsletter');
}
// Filter by IDs if provided
if (filter?.ids && filter.ids.length > 0) {
query = query.whereIn('id', filter.ids);
}
query = this.repository.pagination(query, pagination);
return await this.repository.all(query);
}
@@ -1,6 +1,7 @@
import { Chat } from '@adiwajshing/baileys';
import { NowebChatSchema } from '@waha/core/engines/noweb/store/schemas';
import { SqlChatMethods } from '@waha/core/engines/noweb/store/sql/SqlChatMethods';
import { OverviewFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { KnexPaginator } from '@waha/utils/Paginator';
@@ -28,7 +29,8 @@ export class Sqlite3ChatRepository
getAllWithMessages(
pagination: PaginationParams,
broadcast: boolean,
filter?: OverviewFilter,
): Promise<Chat[]> {
return this.methods.getAllWithMessages(pagination, broadcast);
return this.methods.getAllWithMessages(pagination, broadcast, filter);
}
}
@@ -0,0 +1,41 @@
import { NowebLidMapSchema } from '@waha/core/engines/noweb/store/schemas';
import { LimitOffsetParams } from '@waha/structures/pagination.dto';
import { KnexPaginator } from '@waha/utils/Paginator';
import { INowebLidPNRepository, LidToPN } from '../INowebLidPNRepository';
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
export class LidPaginator extends KnexPaginator {
indexes = ['id', 'pn'];
}
export class Sqlite3LidPNRepository
extends NOWEBSqlite3KVRepository<LidToPN>
implements INowebLidPNRepository
{
get schema() {
return NowebLidMapSchema;
}
saveLids(lids: LidToPN[]): Promise<void> {
return this.upsertMany(lids);
}
getAllLids(pagination?: LimitOffsetParams): Promise<LidToPN[]> {
return this.getAll(pagination);
}
getLidsCount(): Promise<number> {
return this.getCount();
}
async findLidByPN(pn: string): Promise<string | null> {
const value = await this.getBy({ pn: pn });
return value?.id || null;
}
async findPNByLid(lid: string): Promise<string | null> {
const value = await this.getBy({ id: lid });
return value?.pn || null;
}
}
@@ -1,8 +1,10 @@
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { INowebLidPNRepository } from '@waha/core/engines/noweb/store/INowebLidPNRepository';
import { Sqlite3GroupRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3GroupRepository';
import { Sqlite3LabelAssociationsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelAssociationsRepository';
import { Sqlite3LabelsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelsRepository';
import { Sqlite3LidPNRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LidPNRepository';
import { Schema } from '@waha/core/storage/Schema';
import { INowebStorage } from '../INowebStorage';
@@ -74,4 +76,8 @@ export class Sqlite3Storage extends INowebStorage {
getMessagesRepository() {
return new Sqlite3MessagesRepository(this.db);
}
getLidPNRepository(): INowebLidPNRepository {
return new Sqlite3LidPNRepository(this.db);
}
}
@@ -18,7 +18,10 @@ const AsyncLock = require('async-lock');
// https://github.com/nodejs/node/issues/26338
// Default pending is 1000, set it to infinity
// https://github.com/rogierschouten/async-lock/issues/63
const fileLock = new AsyncLock({ maxPending: Infinity });
const fileLock = new AsyncLock({
maxPending: Infinity,
maxExecutionTime: 30_000,
});
/**
* stores the full authentication state in a single folder.
+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);
}
+12
View File
@@ -99,6 +99,18 @@ export class WPage extends EventEmitter {
}
}
async reload(options?: any): Promise<any> {
try {
return await this.page.reload(options);
} catch (err) {
this.emit(PAGE_CALL_ERROR_EVENT, {
method: 'reload',
error: err,
});
throw err;
}
}
on(event: string | symbol, listener: (...args: any[]) => void): this {
if (event === PAGE_CALL_ERROR_EVENT) {
super.on(event, listener);
+159 -8
View File
@@ -1,13 +1,16 @@
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';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { TextStatus } from '@waha/structures/status.dto';
import { sleep } from '@waha/utils/promiseTimeout';
import { EventEmitter } from 'events';
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';
@@ -54,6 +57,26 @@ export class WebjsClientCore extends Client {
await this.pupPage.evaluate(LoadWAHA);
}
/**
* @result indicating whether the UX fresh look was successfully hidden.
*/
hideUXFreshLook(): Promise<boolean> {
return this.pupPage.evaluate(() => {
const WAWebUserPrefsUiRefresh = window.require('WAWebUserPrefsUiRefresh');
if (!WAWebUserPrefsUiRefresh) {
return false;
}
if (WAWebUserPrefsUiRefresh.getUiRefreshNuxAcked()) {
return false;
}
WAWebUserPrefsUiRefresh.incrementNuxViewCount();
WAWebUserPrefsUiRefresh.setUiRefreshNuxAcked(true);
const WAWebModalManager = window.require('WAWebModalManager');
WAWebModalManager.ModalManager.close();
return true;
});
}
async attachCustomEventListeners() {
await exposeFunctionIfAbsent(
this.pupPage,
@@ -99,7 +122,7 @@ export class WebjsClientCore extends Client {
}
async createLabel(name: string, color: number): Promise<number> {
const labelId: number = await this.pupPage.evaluate(
const labelId: number = (await this.pupPage.evaluate(
async (name, color) => {
// @ts-ignore
return await window.WAHA.WAWebBizLabelEditingAction.labelAddAction(
@@ -109,7 +132,7 @@ export class WebjsClientCore extends Client {
},
name,
color,
);
)) as any;
return labelId;
}
@@ -136,7 +159,7 @@ export class WebjsClientCore extends Client {
}, label);
}
async getChats(pagination?: PaginationParams) {
async getChats(pagination?: PaginationParams, filter?: { ids?: string[] }) {
if (lodash.isEmpty(pagination)) {
return await super.getChats();
}
@@ -145,10 +168,14 @@ export class WebjsClientCore extends Client {
pagination.limit ||= Infinity;
pagination.offset ||= 0;
const chats = await this.pupPage.evaluate(async (pagination) => {
// @ts-ignore
return await window.WAHA.getChats(pagination);
}, pagination);
const chats = await this.pupPage.evaluate(
async (pagination, filter) => {
// @ts-ignore
return await window.WAHA.getChats(pagination, filter);
},
pagination,
filter,
);
return chats.map((chat) => ChatFactory.create(this, chat));
}
@@ -262,4 +289,128 @@ export class WebjsClientCore extends Client {
return messages.map((m) => new Message(this, m));
}
public async getAllLids(
pagination: PaginationParams,
): Promise<Array<LidToPhoneNumber>> {
const lids: Array<LidToPhoneNumber> = (await this.pupPage.evaluate(
async (pagination) => {
pagination.limit ||= Infinity;
pagination.offset ||= 0;
pagination.sortBy ||= 'lid';
// @ts-ignore
const WAWebApiContact = window.Store.LidUtils;
await WAWebApiContact.warmUpAllLidPnMappings();
const lidMap = WAWebApiContact.lidPnCache['$1'];
const values = Array.from(lidMap.values());
const result = values.map((map) => {
return {
// @ts-ignore
lid: map.lid._serialized,
// @ts-ignore
pn: map.phoneNumber._serialized,
};
});
// @ts-ignore
const paginator = new window.Paginator(pagination);
const page = paginator.apply(result);
return page;
},
pagination,
)) as any;
return lids;
}
public async getLidsCount(): Promise<number> {
const count: number = (await this.pupPage.evaluate(async () => {
// @ts-ignore
const WAWebApiContact = window.Store.LidUtils;
await WAWebApiContact.warmUpAllLidPnMappings();
const lidMap = WAWebApiContact.lidPnCache['$1'];
return lidMap.size;
})) as any;
return count;
}
public async findPNByLid(lid: string): Promise<string> {
const pn = await this.pupPage.evaluate(async (lid) => {
// @ts-ignore
const WAWebApiContact = window.Store.LidUtils;
// @ts-ignore
const WAWebWidFactory = window.Store.WidFactory;
const wid = WAWebWidFactory.createWid(lid);
const result = WAWebApiContact.getPhoneNumber(wid);
return result ? result._serialized : null;
}, lid);
return pn;
}
public async findLIDByPhoneNumber(phoneNumber: string): Promise<string> {
const lid: string = (await this.pupPage.evaluate(async (pn) => {
// @ts-ignore
const WAWebApiContact = window.Store.LidUtils;
// @ts-ignore
const WAWebWidFactory = window.Store.WidFactory;
const wid = WAWebWidFactory.createWid(pn);
const result = WAWebApiContact.getCurrentLid(wid);
return result ? result._serialized : null;
}, phoneNumber)) as any;
return lid;
}
/**
* Presences methods
*/
public async subscribePresence(chatId: string): Promise<void> {
await this.pupPage.evaluate(async (chatId) => {
const d = require;
const WidFactory = d('WAWebWidFactory');
const wid = WidFactory.createWidFromWidLike(chatId);
const chat = d('WAWebChatCollection').ChatCollection.get(wid);
const tc = chat == null ? void 0 : chat.getTcToken();
await d('WAWebContactPresenceBridge').subscribePresence(wid, tc);
}, chatId);
}
private async getCurrentPresence(chatId: string): Promise<WebJSPresence[]> {
const result = await this.pupPage.evaluate(async (chatId) => {
const d = require;
const WidFactory = d('WAWebWidFactory');
const PresenceCollection = d(
'WAWebPresenceCollection',
).PresenceCollection;
const wid = WidFactory.createWidFromWidLike(chatId);
const presence = PresenceCollection.get(wid);
if (!presence) {
return [];
}
let chatstates = [];
if (chatId.endsWith('@c.us')) {
chatstates = [presence.chatstate];
} else {
chatstates = presence.chatstates.getModelsArray();
}
return chatstates.map((chatstate) => {
return {
participant: chatstate.id._serialized,
lastSeen: chatstate.t,
state: chatstate.type,
};
});
}, chatId);
return result;
}
public async getPresence(chatId: string): Promise<WebJSPresence[]> {
await this.sendPresenceAvailable();
await this.subscribePresence(chatId);
await sleep(3_000);
return await this.getCurrentPresence(chatId);
}
}
+7 -1
View File
@@ -4,8 +4,14 @@ exports.LoadWAHA = () => {
'WAWebBizLabelEditingAction',
);
window.WAHA.getChats = async (pagination) => {
window.WAHA.getChats = async (pagination, filter) => {
let chats = window.Store.Chat.getModelsArray().slice();
// Filter chats by IDs if filter is provided
if (filter && filter.ids && filter.ids.length > 0) {
chats = chats.filter((chat) => filter.ids.includes(chat.id._serialized));
}
const paginator = new window.Paginator(pagination);
chats = paginator.apply(chats);
const chatPromises = chats.map((chat) => window.WWebJS.getChatModel(chat));
+1 -1
View File
@@ -23,7 +23,7 @@ interface Me {
lid?: string;
}
function jid(field: any) {
export function jid(field: any) {
if (!field) {
return field;
}
+67
View File
@@ -0,0 +1,67 @@
import { BinaryNode, WAPresence } from '@adiwajshing/baileys';
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
import { WAHAPresenceStatus } from '@waha/structures/enums.dto';
import { WAHAChatPresences } from '@waha/structures/presence.dto';
import { jid } from './ack.webjs';
export function TagPresenceToPresence(node: BinaryNode): WAHAChatPresences {
const { attrs } = node;
const id = jid(attrs.from);
const state =
attrs.type === 'unavailable'
? WAHAPresenceStatus.OFFLINE
: WAHAPresenceStatus.ONLINE;
const lastSeen = attrs.last && attrs.last !== 'deny' ? +attrs.last : null;
return {
id: toCusFormat(id),
presences: [
{
participant: toCusFormat(id),
lastKnownPresence: state,
lastSeen: lastSeen,
},
],
};
}
export function TagChatstateToPresence(node: BinaryNode): WAHAChatPresences {
const { attrs, content } = node;
const id = jid(attrs.from);
const participant = jid(attrs.participant) || jid(attrs.from);
const firstChild = content[0] as BinaryNode;
const type = firstChild.tag as WAPresence;
let status = WAHAPresenceStatus.OFFLINE;
switch (type) {
case 'unavailable':
status = WAHAPresenceStatus.OFFLINE;
break;
case 'available':
status = WAHAPresenceStatus.ONLINE;
break;
case 'paused':
status = WAHAPresenceStatus.PAUSED;
break;
case 'composing':
status = WAHAPresenceStatus.TYPING;
break;
}
if (firstChild.attrs?.media === 'audio') {
status = WAHAPresenceStatus.RECORDING;
}
return {
id: toCusFormat(id),
presences: [
{
participant: toCusFormat(participant),
lastKnownPresence: status,
lastSeen: null,
},
],
};
}
+137 -13
View File
@@ -19,6 +19,10 @@ import {
ToGroupV2UpdateEvent,
} from '@waha/core/engines/webjs/groups.webjs';
import { LocalAuth } from '@waha/core/engines/webjs/LocalAuth';
import {
TagChatstateToPresence,
TagPresenceToPresence,
} from '@waha/core/engines/webjs/presence';
import { WebjsClientCore } from '@waha/core/engines/webjs/WebjsClientCore';
import {
CallErrorEvent,
@@ -55,6 +59,7 @@ import {
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
OverviewFilter,
ReadChatMessagesQuery,
ReadChatMessagesResponse,
} from '@waha/structures/chats.dto';
@@ -93,13 +98,14 @@ import {
SettingsSecurityChangeInfo,
} from '@waha/structures/groups.dto';
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import { ReplyToMessage } from '@waha/structures/message.dto';
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
import {
MessageSource,
WAMessage,
WAMessageReaction,
} from '@waha/structures/responses.dto';
WAHAChatPresences,
WAHAPresenceData,
} from '@waha/structures/presence.dto';
import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
import { MeInfo } from '@waha/structures/sessions.dto';
import { StatusRequest, TextStatus } from '@waha/structures/status.dto';
import {
@@ -112,7 +118,7 @@ import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner';
import * as lodash from 'lodash';
import { ProtocolError } from 'puppeteer';
import { filter, fromEvent, merge, mergeMap, Observable } from 'rxjs';
import { filter, fromEvent, merge, mergeMap, Observable, share } from 'rxjs';
import { map } from 'rxjs/operators';
import {
AuthStrategy,
@@ -127,13 +133,14 @@ import {
Label as WEBJSLabel,
Location,
Message,
MessageAck,
MessageMedia,
Reaction,
WAState,
} from 'whatsapp-web.js';
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
import { WAJSPresenceChatStateType, WebJSPresence } from './types';
export interface WebJSConfig {
webVersion?: string;
cacheType: 'local' | 'none';
@@ -260,7 +267,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
.then(() => {
// Listen for browser disconnected event
this.whatsapp.pupBrowser.on('disconnected', () => {
this.logger.error('The browser has been disconnected');
if (this.shouldRestart) {
this.logger.error('The browser has been disconnected');
} else {
this.logger.info('The browser has been disconnected');
}
this.failed();
});
@@ -425,6 +436,22 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
this.logger.info(`Session '${this.name}' is ready!`);
});
//
// Temp fix for hiding "Fresh look" modal
// https://github.com/devlikeapro/waha/issues/987
//
this.whatsapp.on(Events.READY, async () => {
try {
const hidden = await this.whatsapp.hideUXFreshLook();
if (hidden) {
this.logger.info('"Fresh look" modal has been hidden');
}
} catch (err) {
this.logger.warn('Failed to hide "Fresh look" modal');
this.logger.warn(err, err.stack);
}
});
this.whatsapp.on(Events.AUTHENTICATED, (args) => {
this.qr.save('');
this.logger.info({ args: args }, `Session has been authenticated!`);
@@ -677,7 +704,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
/**
* Chats methods
*/
getChats(pagination: PaginationParams) {
getChats(pagination: PaginationParams, filter: OverviewFilter | null = null) {
switch (pagination.sortBy) {
case ChatSortField.ID:
pagination.sortBy = 'id._serialized';
@@ -686,18 +713,19 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
pagination.sortBy = 't';
break;
}
return this.whatsapp.getChats(pagination);
return this.whatsapp.getChats(pagination, filter);
}
public async getChatsOverview(
pagination: PaginationParams,
filter?: OverviewFilter,
): Promise<ChatSummary[]> {
pagination = {
...pagination,
sortBy: ChatSortField.CONVERSATION_TIMESTAMP,
sortOrder: SortOrder.DESC,
};
const chats = await this.getChats(pagination);
const chats = await this.getChats(pagination, filter);
const promises = [];
for (const chat of chats) {
@@ -926,6 +954,38 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
await contact.unblock();
}
/**
* Lid to Phone Number methods
*/
public async getAllLids(
pagination: PaginationParams,
): Promise<Array<LidToPhoneNumber>> {
return this.whatsapp.getAllLids(pagination);
}
public async getLidsCount(): Promise<number> {
return this.whatsapp.getLidsCount();
}
public async findPNByLid(lid: string): Promise<LidToPhoneNumber> {
const phoneNumber = await this.whatsapp.findPNByLid(lid);
return {
lid: lid,
pn: phoneNumber,
};
}
public async findLIDByPhoneNumber(
phoneNumber: string,
): Promise<LidToPhoneNumber> {
const pn = toCusFormat(phoneNumber);
const lid = await this.whatsapp.findLIDByPhoneNumber(pn);
return {
lid: lid,
pn: pn,
};
}
/**
* Group methods
*/
@@ -1229,6 +1289,52 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
}
public getPresences(): Promise<WAHAChatPresences[]> {
throw new NotImplementedByEngineError();
}
public async getPresence(id: string): Promise<WAHAChatPresences> {
const chatId = toCusFormat(id);
const presences = await this.whatsapp.getPresence(chatId);
return this.toWahaPresences(chatId, presences);
}
public async subscribePresence(id: string): Promise<any> {
const chatId = toCusFormat(id);
await this.whatsapp.subscribePresence(chatId);
}
private toWahaPresences(
chatId: string,
data: WebJSPresence[],
): WAHAChatPresences {
const presences: WAHAPresenceData[] = data.map((presence) => {
let status: WAHAPresenceStatus = WAHAPresenceStatus.OFFLINE;
switch (presence.state) {
case WAJSPresenceChatStateType.AVAILABLE:
status = WAHAPresenceStatus.ONLINE;
break;
case WAJSPresenceChatStateType.UNAVAILABLE:
status = WAHAPresenceStatus.OFFLINE;
break;
case WAJSPresenceChatStateType.TYPING:
status = WAHAPresenceStatus.TYPING;
break;
case WAJSPresenceChatStateType.RECORDING_AUDIO:
status = WAHAPresenceStatus.RECORDING;
}
return {
participant: presence.participant,
lastSeen: presence.lastSeen || null,
lastKnownPresence: status,
};
});
return {
id: toCusFormat(chatId),
presences: presences,
};
}
/**
* Status methods
*/
@@ -1358,6 +1464,22 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
const stateChanged$ = fromEvent(this.whatsapp, Events.STATE_CHANGED);
this.events2.get(WAHAEvents.STATE_CHANGE).switch(stateChanged$);
//
// Presence
//
const tagPresenceNode$ = fromEvent(this.whatsapp, Events.TAG_PRESENCE);
const presences$ = tagPresenceNode$.pipe(
map(TagPresenceToPresence),
filter(Boolean),
);
const tagChatstateNode$ = fromEvent(this.whatsapp, 'tag:chatstate');
const chatstatePresences$ = tagChatstateNode$.pipe(
map(TagChatstateToPresence),
filter(Boolean),
);
const presenceUpdate$ = merge(presences$, chatstatePresences$);
this.events2.get(WAHAEvents.PRESENCE_UPDATE).switch(presenceUpdate$);
//
// Groups
//
@@ -1555,10 +1677,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
public async getEngineInfo() {
// Add 1 seconds timeout
if (!this.whatsapp || !this.whatsapp.pupPage) {
return null;
}
return {
WWebVersion: await this.whatsapp?.getWWebVersion(),
state: await this.whatsapp?.getState(),
WWebVersion: await this.whatsapp.getWWebVersion(),
state: await this.whatsapp.getState(),
};
}
+12
View File
@@ -0,0 +1,12 @@
export enum WAJSPresenceChatStateType {
AVAILABLE = 'available', // User is online
UNAVAILABLE = 'unavailable', // User went offline (may include `t`)
TYPING = 'typing', // User is typing text
RECORDING_AUDIO = 'recording_audio', // User is recording a voice message
}
export interface WebJSPresence {
participant: string;
lastSeen?: number;
state: WAJSPresenceChatStateType;
}
+10 -7
View File
@@ -1,20 +1,23 @@
export function toVcard(data): string {
import { Contact, VCardContact } from '@waha/structures/chatting.dto';
export function toVcard(data: Contact | VCardContact): string {
if (data.vcard) {
return data.vcard;
}
const contact: Contact = data as any;
const parts = [];
parts.push('BEGIN:VCARD');
parts.push('VERSION:3.0');
parts.push(`FN:${data.fullName}`);
if (data.organization) {
parts.push(`ORG:${data.organization};`);
parts.push(`FN:${contact.fullName}`);
if (contact.organization) {
parts.push(`ORG:${contact.organization};`);
}
if (data.whatsappId) {
if (contact.whatsappId) {
parts.push(
`TEL;type=CELL;type=VOICE;waid=${data.whatsappId}:${data.phoneNumber}`,
`TEL;type=CELL;type=VOICE;waid=${contact.whatsappId}:${contact.phoneNumber}`,
);
} else {
parts.push(`TEL;type=CELL;type=VOICE:${data.phoneNumber}`);
parts.push(`TEL;type=CELL;type=VOICE:${contact.phoneNumber}`);
}
parts.push('END:VCARD');
return parts.join('\n');
+6 -7
View File
@@ -1,6 +1,5 @@
import * as crypto from 'crypto';
import { safeJoin } from '@waha/utils/files';
import * as fs from 'fs/promises';
import * as os from 'os';
import * as path from 'path';
import { LocalStore } from './LocalStore';
@@ -33,14 +32,14 @@ export class LocalStoreCore extends LocalStore {
* Get the directory where all the engines and sessions are stored
*/
getBaseDirectory() {
return path.join(this.baseDirectory);
return path.resolve(this.baseDirectory);
}
/**
* Get the directory where the engine sessions are stored
*/
getEngineDirectory() {
return path.join(this.baseDirectory, this.engine);
return safeJoin(this.baseDirectory, this.engine);
}
getSessionDirectory(name: string): string {
@@ -48,17 +47,17 @@ export class LocalStoreCore extends LocalStore {
}
getFilePath(session: string, file: string): string {
return path.join(this.getSessionDirectory(session), file);
return safeJoin(this.getSessionDirectory(session), file);
}
protected getDirectoryPath(name: string): string {
return path.join(this.getEngineDirectory(), name);
return safeJoin(this.getEngineDirectory(), name);
}
getWAHADatabase(): any {
if (!this.db) {
const engineDir = this.getEngineDirectory();
const database = path.join(engineDir, 'waha.sqlite3');
const database = safeJoin(engineDir, 'waha.sqlite3');
this.db = new Database(database);
this.db.pragma('journal_mode = WAL;');
}
+14 -5
View File
@@ -95,11 +95,11 @@ export class SqlKVRepository<Entity> {
const sql = `INSERT INTO "${this.table}" (${columns.join(', ')})
VALUES ${data
.map(() => `(${columns.map(() => '?').join(', ')})`)
.join(', ')}
ON CONFLICT(id) DO UPDATE
SET ${columns
.map((column) => `${column} = excluded.${column}`)
.join(', ')}`;
.join(', ')} ON CONFLICT(id) DO
UPDATE
SET ${columns
.map((column) => `${column} = excluded.${column}`)
.join(', ')}`;
try {
await this.raw(sql, values);
} catch (err) {
@@ -119,6 +119,15 @@ export class SqlKVRepository<Entity> {
return this.all(query);
}
async getCount(): Promise<number> {
const query = this.select().count({ count: 'id' });
const row = await this.engine.get(query);
if (!row) {
return 0;
}
return parseInt(row.count, 10);
}
async getAllByIds(ids: string[]) {
const entitiesMap = await this.getEntitiesByIds(ids);
return Array.from(entitiesMap.values()).filter(
+16
View File
@@ -0,0 +1,16 @@
import { EventResponseType } from '@waha/structures/events.dto';
export function ParseEventResponseType(response: number) {
switch (response) {
case 0:
return EventResponseType.UNKNOWN;
case 1:
return EventResponseType.GOING;
case 2:
return EventResponseType.NOT_GOING;
case 3:
return EventResponseType.MAYBE;
default:
return EventResponseType.UNKNOWN;
}
}
+33 -1
View File
@@ -7,8 +7,17 @@ import {
PaginationParams,
} from '@waha/structures/pagination.dto';
import { ChatIdProperty } from '@waha/structures/properties.dto';
import { SessionConfig } from '@waha/structures/sessions.dto';
import { Transform, Type } from 'class-transformer';
import { IsBoolean, IsEnum, IsNumber, IsOptional } from 'class-validator';
import {
IsArray,
IsBoolean,
IsEnum,
IsNumber,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
/**
* Queries
@@ -187,6 +196,29 @@ export class OverviewPaginationParams extends LimitOffsetParams {
limit?: number = 20;
}
export class OverviewFilter {
@IsOptional()
@IsArray()
@IsString({ each: true })
@Transform(({ value }) => (Array.isArray(value) ? value : [value]))
@ApiProperty({
description: 'Filter by chat ids',
required: false,
example: ['111111111@c.us'],
})
ids?: string[];
}
export class OverviewBodyRequest {
@ValidateNested()
@Type(() => OverviewPaginationParams)
pagination: OverviewPaginationParams;
@ValidateNested()
@Type(() => OverviewFilter)
filter: OverviewFilter;
}
export class ChatSummary {
id: string;
name: string | null;
+4
View File
@@ -164,6 +164,9 @@ export class MessageContactVcardRequest extends ChatRequest {
],
})
contacts: (VCardContact | Contact)[];
@ReplyToProperty()
reply_to?: string;
}
export class MessageTextRequest extends ChatRequest {
@@ -417,6 +420,7 @@ export class MessageDestination {
to: string;
from: string;
fromMe: boolean;
participant?: string;
}
export class MessageButtonReply extends ChatRequest {
+2
View File
@@ -26,6 +26,8 @@ export enum WAHAEvents {
LABEL_DELETED = 'label.deleted',
LABEL_CHAT_ADDED = 'label.chat.added',
LABEL_CHAT_DELETED = 'label.chat.deleted',
EVENT_RESPONSE = 'event.response',
EVENT_RESPONSE_FAILED = 'event.response.failed',
ENGINE_EVENT = 'engine.event',
}
+144
View File
@@ -0,0 +1,144 @@
import { ApiProperty } from '@nestjs/swagger';
import { MessageDestination } from '@waha/structures/chatting.dto';
import { WAMessageBase } from '@waha/structures/responses.dto';
import { Type } from 'class-transformer';
import {
IsBoolean,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
import { ChatIdProperty, ReplyToProperty } from './properties.dto';
export class EventLocation {
@ApiProperty({
description: 'Name of the location',
example: 'Luxe Nail Studio 💅',
})
@IsString()
@IsNotEmpty()
name: string;
//
// Doesn't work right now
//
// @ApiProperty({
// description: 'Latitude of the location',
// example: 38.8937255,
// })
// @IsNumber()
// @IsOptional()
// degreesLatitude?: number;
//
// @ApiProperty({
// description: 'Longitude of the location',
// example: -77.0969763,
// })
// @IsNumber()
// @IsOptional()
// degreesLongitude?: number;
}
export class EventMessage {
@ApiProperty({
description: 'Name of the event',
example: "John's Nail Appointment 💅",
})
@IsString()
@IsNotEmpty()
name: string;
@ApiProperty({
description: 'Description of the event',
example:
"It's time for your nail care session! 🌟\\n\\nYou'll be getting a *classic gel manicure* – clean, polished, and long-lasting. 💖\\n\\n📍 *Location:* Luxe Nail Studio\\nWe're on the *2nd floor of the Plaza Mall*, next to the flower shop. Look for the *pink neon sign*!\\n\\nFeel free to arrive *5–10 mins early* so we can get started on time 😊",
required: false,
})
@IsString()
@IsOptional()
description?: string;
@ApiProperty({
description: 'Start time of the event (Unix timestamp in seconds)',
example: 2063137000,
})
@IsNumber()
@IsNotEmpty()
startTime: number;
@ApiProperty({
description: 'End time of the event (Unix timestamp in seconds)',
example: null,
required: false,
})
@IsNumber()
@IsOptional()
endTime?: number;
@ApiProperty({
description: 'Location of the event',
required: false,
type: EventLocation,
})
@ValidateNested()
@Type(() => EventLocation)
@IsOptional()
location?: EventLocation;
@ApiProperty({
description: 'Whether extra guests are allowed',
example: false,
required: false,
})
@IsBoolean()
@IsOptional()
extraGuestsAllowed?: boolean;
}
export class EventMessageRequest {
@ChatIdProperty()
chatId: string;
event: EventMessage;
@ReplyToProperty()
reply_to?: string;
}
export class EventCancelRequest {
@ApiProperty({
description: 'ID of the event message to cancel',
example: 'true_12345678901@c.us_ABCDEFGHIJKLMNOPQRST',
})
@IsString()
@IsNotEmpty()
id: string;
}
export enum EventResponseType {
UNKNOWN = 'UNKNOWN',
GOING = 'GOING',
NOT_GOING = 'NOT_GOING',
MAYBE = 'MAYBE',
}
export class EventResponse {
response: EventResponseType;
timestampMs: number;
extraGuestCount: number;
}
export class EventResponsePayload extends WAMessageBase {
eventCreationKey: MessageDestination;
eventResponse?: EventResponse;
/** Returns a message in a raw format */
@ApiProperty({
description:
'Message in a raw format that we get from WhatsApp. May be changed anytime, use it with caution! It depends a lot on the underlying backend.',
})
_data?: any;
}
+21
View File
@@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import { LimitOffsetParams } from '@waha/structures/pagination.dto';
export class LidToPhoneNumber {
@ApiProperty({
description: 'Linked ID for the user',
example: '1111111@lid',
})
lid?: string;
@ApiProperty({
description: 'Phone number (chat id) for the user',
example: '3333333@c.us',
})
pn?: string;
}
export class LidsListQueryParams extends LimitOffsetParams {
limit?: number = 100;
offset?: number = 0;
}
+1 -1
View File
@@ -16,7 +16,7 @@ export enum MessageSource {
APP = 'app',
}
class WAMessageBase {
export class WAMessageBase {
@MessageIdProperty()
id: string;
+21
View File
@@ -1,5 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { CallData } from '@waha/structures/calls.dto';
import { EventResponsePayload } from '@waha/structures/events.dto';
import { Label, LabelChatAssociation } from '@waha/structures/labels.dto';
import { ChatArchiveEvent } from './chats.dto';
@@ -86,6 +87,7 @@ export class PollVote extends MessageDestination {
export class PollVotePayload {
vote: PollVote;
poll: MessageDestination;
_data?: any;
}
export class WAMessageRevokedBody {
@@ -344,6 +346,25 @@ export class EnginePayload {
data: any;
}
export class WAHAWebhookEventResponse extends WAHAWebhook {
@ApiProperty({
description: 'The event is triggered when the event response is received.',
})
event = WAHAEvents.EVENT_RESPONSE;
payload: EventResponsePayload;
}
export class WAHAWebhookEventResponseFailed extends WAHAWebhook {
@ApiProperty({
description:
'The event is triggered when the event response is failed to decrypt.',
})
event = WAHAEvents.EVENT_RESPONSE_FAILED;
payload: EventResponsePayload;
}
export class WAHAWebhookEngineEvent extends WAHAWebhook {
@ApiProperty({
description: 'Internal engine event.',
+4
View File
@@ -10,6 +10,8 @@ import {
WAHAWebhookCallRejected,
WAHAWebhookChatArchive,
WAHAWebhookEngineEvent,
WAHAWebhookEventResponse,
WAHAWebhookEventResponseFailed,
WAHAWebhookGroupJoin,
WAHAWebhookGroupLeave,
WAHAWebhookLabelChatAdded,
@@ -50,6 +52,8 @@ const WAHA_WEBHOOKS = [
WAHAWebhookLabelDeleted,
WAHAWebhookLabelChatAdded,
WAHAWebhookLabelChatDeleted,
WAHAWebhookEventResponse,
WAHAWebhookEventResponseFailed,
WAHAWebhookEngineEvent,
WAHAWebhookGroupJoin,
WAHAWebhookGroupLeave,
+26
View File
@@ -1,3 +1,5 @@
import * as path from 'path';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs-extra');
@@ -9,3 +11,27 @@ export async function fileExists(filepath: string) {
}
return true;
}
export function safeJoin(base: string, input: string): string {
base = path.resolve(base);
if (!input || typeof input !== 'string') {
throw new Error('Invalid path');
}
if (input.startsWith('~') || path.isAbsolute(input)) {
throw new Error('Home or absolute paths not allowed');
}
// handles slashes safely
const joined = path.join(base, input);
// normalize to an absolute path
const resolved = path.resolve(joined);
// Prevent escape outside base dir
if (!resolved.startsWith(base + path.sep)) {
throw new Error('Access outside base dir not allowed');
}
return resolved;
}
+1 -1
View File
@@ -33,7 +33,7 @@ export function getEngineName(): string {
}
export const VERSION: WAHAEnvironment = {
version: '2025.5.3',
version: '2025.6.1',
engine: getEngineName(),
tier: getWAHAVersion(),
browser:
+2 -2
View File
@@ -2,11 +2,11 @@
"waha": {
"gows": {
"repo": "devlikeapro/gows",
"ref": "v1.0.5"
"ref": "v1.0.8"
},
"dashboard": {
"repo": "devlikeapro/dashboard",
"ref": "8f156ca15f460c68bea87535cf6d9f5c51c07f1f"
"ref": "3fb6694d849ca0b567679dd59311a50b9d727f2f"
}
}
}
+1420 -820
View File
File diff suppressed because it is too large. Load diff