Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d8c6c0a62 | ||
|
|
17b26b869b | ||
|
|
dfbe54e92f | ||
|
|
2321de409b | ||
|
|
502287a258 | ||
|
|
052ff0ea6e | ||
|
|
2b76789cdf | ||
|
|
7bbf5df231 | ||
|
|
794a04e7d3 | ||
|
|
645467f13a | ||
|
|
7e87637341 | ||
|
|
b7abeed54a | ||
|
|
d53646eef2 | ||
|
|
c54743de6c | ||
|
|
e1fdc41132 | ||
|
|
060aa82294 | ||
|
|
2df08eac3f | ||
|
|
0ecdb9d3d0 | ||
|
|
6a7fe1bd1f | ||
|
|
87c69cb3d0 | ||
|
|
c654855c22 | ||
|
|
11bb006801 | ||
|
|
e5551f3f64 | ||
|
|
0de148005f | ||
|
|
27e4ce628e | ||
|
|
19b752bf15 | ||
|
|
6528e5bba9 | ||
|
|
37e1183e87 | ||
|
|
824512cf8e | ||
|
|
82f296b17d | ||
|
|
8302530954 | ||
|
|
6c271829aa | ||
|
|
3b30d339ee | ||
|
|
c5a268bdbd | ||
|
|
8457ea8e47 | ||
|
|
72d69eeab5 | ||
|
|
31885e41af | ||
|
|
e63726d18b | ||
|
|
3d2ab45d25 | ||
|
|
246478e1d8 | ||
|
|
9418de3b23 | ||
|
|
c6b68cad37 | ||
|
|
ef36e4d3ac | ||
|
|
ba2ddd62ad | ||
|
|
6a03e1488c | ||
|
|
cabd2ed562 | ||
|
|
4a9296276d | ||
|
|
1fd84d824c | ||
|
|
faa55ffe2d | ||
|
|
d45af4191f | ||
|
|
9f8dead8b8 | ||
|
|
564037cd53 | ||
|
|
434de6292c | ||
|
|
a12e6990e5 | ||
|
|
ce0c4a094b | ||
|
|
e36728ec08 | ||
|
|
2c1f3090a0 | ||
|
|
0c6d4b3581 | ||
|
|
1d5b9eb241 | ||
|
|
b0567dafe3 | ||
|
|
518f78f067 | ||
|
|
47a2c21824 | ||
|
|
aca5547965 | ||
|
|
0e07876630 | ||
|
|
a43901b4a4 | ||
|
|
6515fac16d | ||
|
|
fdaa7e9f48 | ||
|
|
cd779850de | ||
|
|
ef939f4357 | ||
|
|
bfb9dd7009 | ||
|
|
f282f14eff | ||
|
|
992b289500 | ||
|
|
9dcdebcab2 | ||
|
|
c0b8bacab0 | ||
|
|
f96e6420fc | ||
|
|
d7f97480cc | ||
|
|
26df41b571 | ||
|
|
4ca336017d | ||
|
|
94252ea54c | ||
|
|
f8220fec1f | ||
|
|
a5ded396c9 | ||
|
|
c457cf8064 | ||
|
|
3079f90118 | ||
|
|
4fbb331015 | ||
|
|
640c0d305c | ||
|
|
0b044c67bb | ||
|
|
7a37ba8145 | ||
|
|
b447863d9f | ||
|
|
6dd7e14f6f | ||
|
|
b09ef98dce | ||
|
|
ff1cf8a288 | ||
|
|
603793662a | ||
|
|
4cdab0dbc7 |
No files matched your search
+5
-5
@@ -23,9 +23,9 @@ WAHA_BASE_URL=http://localhost:3000
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/security/
|
||||
#
|
||||
WHATSAPP_API_KEY=321
|
||||
WAHA_DASHBOARD_USERNAME=waha
|
||||
WAHA_DASHBOARD_PASSWORD=waha
|
||||
WHATSAPP_API_KEY=admin
|
||||
WAHA_DASHBOARD_USERNAME=admin
|
||||
WAHA_DASHBOARD_PASSWORD=admin
|
||||
#WHATSAPP_SWAGGER_USERNAME=admin
|
||||
#WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
|
||||
@@ -35,8 +35,8 @@ WAHA_DASHBOARD_PASSWORD=waha
|
||||
# https://waha.devlike.pro/docs/how-to/waha-dashboard/#configuration
|
||||
# Enabled by default
|
||||
#WAHA_DASHBOARD_ENABLED=true
|
||||
#WAHA_DASHBOARD_USERNAME=waha
|
||||
#WAHA_DASHBOARD_PASSWORD=waha
|
||||
#WAHA_DASHBOARD_USERNAME=admin
|
||||
#WAHA_DASHBOARD_PASSWORD=admin
|
||||
|
||||
#
|
||||
# Logging
|
||||
|
||||
+6
-1
@@ -131,7 +131,7 @@ RUN if [ "$USE_BROWSER" = "chromium" ]; then \
|
||||
# Install Chrome
|
||||
# Available versions:
|
||||
# https://www.ubuntuupdates.org/package/google_chrome/stable/main/base/google-chrome-stable
|
||||
ARG CHROME_VERSION="130.0.6723.69-1"
|
||||
ARG CHROME_VERSION="135.0.7049.114-1"
|
||||
RUN if [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
wget --no-verbose -O /tmp/chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb \
|
||||
&& apt-get update \
|
||||
@@ -146,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"]
|
||||
@@ -35,13 +35,13 @@ for-swagger:
|
||||
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true . ${NVM_DIR}/nvm.sh && nvm exec yarn start
|
||||
|
||||
up-noweb:
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-channels-6712
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2025-05-01
|
||||
|
||||
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
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -11,8 +11,8 @@ services:
|
||||
- './.media:/app/.media'
|
||||
environment:
|
||||
- WHATSAPP_API_KEY=321
|
||||
- WAHA_DASHBOARD_USERNAME=waha
|
||||
- WAHA_DASHBOARD_PASSWORD=waha
|
||||
- WAHA_DASHBOARD_USERNAME=admin
|
||||
- WAHA_DASHBOARD_PASSWORD=admin
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
- WAHA_PRINT_QR=False
|
||||
- WAHA_MEDIA_STORAGE=LOCAL
|
||||
|
||||
@@ -36,8 +36,8 @@ services:
|
||||
# Username and password for WAHA Dashboard
|
||||
# you can use the same username and password for both Swagger and Dashboard
|
||||
# Default - waha / waha
|
||||
- WAHA_DASHBOARD_USERNAME=waha
|
||||
- WAHA_DASHBOARD_PASSWORD=waha
|
||||
- WAHA_DASHBOARD_USERNAME=admin
|
||||
- WAHA_DASHBOARD_PASSWORD=admin
|
||||
# Engine specific environment variables
|
||||
# - WAHA_WEBJS_WEB_VERSION=2.2412.54-videofix
|
||||
restart: always
|
||||
|
||||
+1
-1
@@ -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
|
||||
+19
-16
@@ -26,27 +26,29 @@
|
||||
"gows:proto": "yarn gows:proto:fetch && yarn gows:proto:build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-channels-6712",
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2025-05-01",
|
||||
"@adiwajshing/keyed-db": "^0.2.4",
|
||||
"@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",
|
||||
"async-lock": "^1.4.1",
|
||||
"audio-decode": "^2.2.2",
|
||||
"axios": "^1.7.7",
|
||||
"axios-retry": "^4.5.0",
|
||||
"better-sqlite3": "11.3.0",
|
||||
@@ -69,7 +71,8 @@
|
||||
"mime-types": "^2.1.27",
|
||||
"mongodb": "6.9.0",
|
||||
"nestjs-pino": "^4.1.0",
|
||||
"passport": "^0.6.0",
|
||||
"node-cache": "5.1.2",
|
||||
"passport": "^0.7.0",
|
||||
"passport-headerapikey": "^1.2.2",
|
||||
"pg": "^8.13.1",
|
||||
"pino-http": "^10.2.0",
|
||||
@@ -85,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": {
|
||||
@@ -94,16 +97,16 @@
|
||||
"resolutions": {
|
||||
"bufferutil": "^4.0.8",
|
||||
"ws": "^8.18.0",
|
||||
"puppeteer": "^23.6.0",
|
||||
"puppeteer": "^24.7.2",
|
||||
"whatwg-url": "13.0.0",
|
||||
"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",
|
||||
@@ -114,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",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Set source and output file
|
||||
SOURCE_DIR="src"
|
||||
OUTPUT_DIR="$HOME/Downloads/chatgpt"
|
||||
OUTPUT_FILE="$OUTPUT_DIR/waha.ts"
|
||||
|
||||
# Create or clear the output file
|
||||
rm -f "$OUTPUT_FILE"
|
||||
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 "\n-------" >> "$OUTPUT_FILE"
|
||||
echo "File: $file" >> "$OUTPUT_FILE"
|
||||
echo "-------\n" >> "$OUTPUT_FILE"
|
||||
# Append the content of the file to the output file
|
||||
cat "$file" >> "$OUTPUT_FILE"
|
||||
done
|
||||
|
||||
echo "All .ts files combined into $OUTPUT_FILE with separators."
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiParam, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ChannelsInfoServiceCore } from '@waha/core/services/ChannelsInfoServiceCore';
|
||||
import { isJidNewsletter } from '@waha/core/utils/jids';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
@@ -35,7 +36,6 @@ import {
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import {
|
||||
isNewsletter,
|
||||
parseChannelInviteLink,
|
||||
WhatsappSession,
|
||||
} from '../core/abc/session.abc';
|
||||
@@ -93,7 +93,7 @@ export class ChannelsController {
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<Channel> {
|
||||
if (isNewsletter(id)) {
|
||||
if (isJidNewsletter(id)) {
|
||||
return session.channelsGetChannel(id);
|
||||
} else {
|
||||
const inviteCode = parseChannelInviteLink(id);
|
||||
@@ -126,7 +126,7 @@ export class ChannelsController {
|
||||
@Param('id') code: string,
|
||||
@Query() query: PreviewChannelMessages,
|
||||
): Promise<ChannelMessage[]> {
|
||||
if (isNewsletter(code)) {
|
||||
if (isJidNewsletter(code)) {
|
||||
const channel = await session.channelsGetChannel(code);
|
||||
code = parseChannelInviteLink(channel.invite);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isJidGroup } from '@adiwajshing/baileys';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
@@ -29,8 +30,13 @@ import {
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
OverviewBodyRequest,
|
||||
OverviewFilter,
|
||||
OverviewPaginationParams,
|
||||
PinMessageRequest,
|
||||
ReadChatMessagesQuery,
|
||||
ReadChatMessagesResponse,
|
||||
transformAck,
|
||||
} from '../structures/chats.dto';
|
||||
import { EditMessageRequest } from '../structures/chatting.dto';
|
||||
|
||||
@@ -61,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')
|
||||
@@ -100,9 +121,23 @@ class ChatsController {
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
) {
|
||||
filter = transformAck(filter);
|
||||
return session.getChatMessages(chatId, query, filter);
|
||||
}
|
||||
|
||||
@Post(':chatId/messages/read')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Read unread messages in the chat' })
|
||||
@ChatIdApiParam
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
readChatMessages(
|
||||
@Query() query: ReadChatMessagesQuery,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
): Promise<ReadChatMessagesResponse> {
|
||||
return session.readChatMessages(chatId, query);
|
||||
}
|
||||
|
||||
@Get(':chatId/messages/:messageId')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Gets message by id' })
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import {
|
||||
GetChatMessagesFilter,
|
||||
ReadChatMessagesQuery,
|
||||
transformAck,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
@@ -136,6 +140,15 @@ export class ChattingController {
|
||||
|
||||
@Post('/sendSeen')
|
||||
async sendSeen(@Body() chat: SendSeenRequest) {
|
||||
const hasMessageId = chat.messageIds?.length > 0 || Boolean(chat.messageId);
|
||||
if (!hasMessageId) {
|
||||
const whatsapp = await this.manager.getWorkingSession(chat.session);
|
||||
const query: ReadChatMessagesQuery = {
|
||||
messages: null,
|
||||
days: 7,
|
||||
};
|
||||
return whatsapp.readChatMessages(chat.chatId, query);
|
||||
}
|
||||
const whatsapp = await this.manager.getWorkingSession(chat.session);
|
||||
return whatsapp.sendSeen(chat);
|
||||
}
|
||||
@@ -223,6 +236,7 @@ export class ChattingController {
|
||||
@Query() query: GetMessageQuery,
|
||||
@Query() filter: GetChatMessagesFilter,
|
||||
) {
|
||||
filter = transformAck(filter);
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
return whatsapp.getChatMessages(query.chatId, query, filter);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
// }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { isJidBroadcast } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
|
||||
import { MessagesForRead } from '@waha/core/utils/convertors';
|
||||
import { isJidNewsletter } from '@waha/core/utils/jids';
|
||||
import {
|
||||
Channel,
|
||||
ChannelListResult,
|
||||
@@ -14,11 +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';
|
||||
@@ -71,6 +80,10 @@ import {
|
||||
WAHAPresenceStatus,
|
||||
WAHASessionStatus,
|
||||
} from '../../structures/enums.dto';
|
||||
import {
|
||||
EventCancelRequest,
|
||||
EventMessageRequest,
|
||||
} from '../../structures/events.dto';
|
||||
import {
|
||||
CreateGroupRequest,
|
||||
GroupField,
|
||||
@@ -462,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
|
||||
*/
|
||||
@@ -471,6 +492,7 @@ export abstract class WhatsappSession {
|
||||
|
||||
public getChatsOverview(
|
||||
pagination: PaginationParams,
|
||||
filter?: OverviewFilter,
|
||||
): Promise<ChatSummary[]> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
@@ -483,10 +505,35 @@ export abstract class WhatsappSession {
|
||||
chatId: string,
|
||||
query: GetChatMessagesQuery,
|
||||
filter: GetChatMessagesFilter,
|
||||
) {
|
||||
): Promise<WAMessage[]> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
abstract readChatMessages(
|
||||
chatId: string,
|
||||
request: ReadChatMessagesQuery,
|
||||
): Promise<ReadChatMessagesResponse>;
|
||||
|
||||
protected async readChatMessagesWSImpl(
|
||||
chatId: string,
|
||||
request: ReadChatMessagesQuery,
|
||||
): Promise<ReadChatMessagesResponse> {
|
||||
const { query, filter } = MessagesForRead(chatId, request);
|
||||
const messages = await this.getChatMessages(chatId, query, filter);
|
||||
this.logger.debug(`Found ${messages.length} messages to read`);
|
||||
if (messages.length === 0) {
|
||||
return { ids: [] };
|
||||
}
|
||||
const ids = messages.map((m) => m.id);
|
||||
const seen: SendSeenRequest = {
|
||||
chatId: chatId,
|
||||
messageIds: ids,
|
||||
session: '',
|
||||
};
|
||||
await this.sendSeen(seen);
|
||||
return { ids: ids };
|
||||
}
|
||||
|
||||
public getChatMessage(
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
@@ -586,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
|
||||
@@ -607,7 +677,7 @@ export abstract class WhatsappSession {
|
||||
let fn: Promise<string>;
|
||||
if (isJidBroadcast(id)) {
|
||||
return null;
|
||||
} else if (isNewsletter(id)) {
|
||||
} else if (isJidNewsletter(id)) {
|
||||
fn = this.channelsGetChannel(id).then(
|
||||
(channel: Channel) => channel.picture || channel.preview,
|
||||
);
|
||||
@@ -925,10 +995,6 @@ export abstract class WhatsappSession {
|
||||
}
|
||||
}
|
||||
|
||||
export function isNewsletter(jid: string) {
|
||||
return jid.endsWith('@newsletter');
|
||||
}
|
||||
|
||||
export function getGroupInviteLink(code: string) {
|
||||
if (code.startsWith('https://')) {
|
||||
return code;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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...`);
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function isFromFullSync(event: any): boolean {
|
||||
return event.FromFullSync;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
GroupInfoFull,
|
||||
JoinedGroupEvent,
|
||||
} from '@waha/core/engines/gows/types.group';
|
||||
import { toJID } from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import { toJID } from '@waha/core/utils/jids';
|
||||
import {
|
||||
GroupId,
|
||||
GroupInfo,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
import {
|
||||
aggregateMessageKeysNotFromMe,
|
||||
getContentType,
|
||||
getUrlFromDirectPath,
|
||||
isJidGroup,
|
||||
jidNormalizedUser,
|
||||
normalizeMessageContent,
|
||||
WAMessageKey,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { isJidBroadcast } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
|
||||
import * as grpc from '@grpc/grpc-js';
|
||||
import { connectivityState } from '@grpc/grpc-js';
|
||||
import { UnprocessableEntityException } from '@nestjs/common';
|
||||
@@ -30,9 +34,8 @@ import {
|
||||
} from '@waha/core/engines/gows/helpers';
|
||||
import { GowsAuthFactoryCore } from '@waha/core/engines/gows/store/GowsAuthFactoryCore';
|
||||
import {
|
||||
parseMessageIdSerialized,
|
||||
getDestination,
|
||||
toCusFormat,
|
||||
toJID,
|
||||
} from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import { extractMediaContent } from '@waha/core/engines/noweb/utils';
|
||||
import {
|
||||
@@ -41,6 +44,9 @@ import {
|
||||
} from '@waha/core/exceptions';
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { QR } from '@waha/core/QR';
|
||||
import { ExtractMessageKeysForRead } from '@waha/core/utils/convertors';
|
||||
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
|
||||
import { toJID } from '@waha/core/utils/jids';
|
||||
import {
|
||||
Channel,
|
||||
ChannelListResult,
|
||||
@@ -58,16 +64,21 @@ import {
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
OverviewFilter,
|
||||
ReadChatMessagesQuery,
|
||||
ReadChatMessagesResponse,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import {
|
||||
ChatRequest,
|
||||
CheckNumberStatusQuery,
|
||||
EditMessageRequest,
|
||||
MessageContactVcardRequest,
|
||||
MessageFileRequest,
|
||||
MessageForwardRequest,
|
||||
MessageImageRequest,
|
||||
MessageLinkCustomPreviewRequest,
|
||||
MessageLocationRequest,
|
||||
MessagePollRequest,
|
||||
MessageReactionRequest,
|
||||
MessageReplyRequest,
|
||||
MessageTextRequest,
|
||||
@@ -83,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,
|
||||
@@ -91,6 +108,7 @@ import {
|
||||
ParticipantsRequest,
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '@waha/structures/groups.dto';
|
||||
import { ReplyToMessage } from '@waha/structures/message.dto';
|
||||
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
|
||||
import {
|
||||
WAHAChatPresences,
|
||||
@@ -105,18 +123,19 @@ import { MeInfo, ProxyConfig } from '@waha/structures/sessions.dto';
|
||||
import {
|
||||
BROADCAST_ID,
|
||||
DeleteStatusRequest,
|
||||
StatusRequest,
|
||||
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';
|
||||
import * as NodeCache from 'node-cache';
|
||||
import {
|
||||
debounceTime,
|
||||
filter,
|
||||
groupBy,
|
||||
merge,
|
||||
mergeMap,
|
||||
Observable,
|
||||
@@ -128,9 +147,24 @@ import { map } from 'rxjs/operators';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import * as gows from './types';
|
||||
import { MessageStatus } from './types';
|
||||
import MessageServiceClient = messages.MessageServiceClient;
|
||||
import { isJidBroadcast } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
|
||||
import { ReplyToMessage } from '@waha/structures/message.dto';
|
||||
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',
|
||||
@@ -147,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 = {
|
||||
@@ -347,10 +388,18 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
|
||||
let [messagesFromMe$, messagesFromOthers$] = partition(messages$, isMine);
|
||||
messagesFromMe$ = messagesFromMe$.pipe(
|
||||
map((msg) => {
|
||||
msg.Status = MessageStatus.ServerAck;
|
||||
return msg;
|
||||
}),
|
||||
mergeMap((msg) => this.processIncomingMessage(msg, true)),
|
||||
share(), // share it so we don't process twice in message.any
|
||||
);
|
||||
messagesFromOthers$ = messagesFromOthers$.pipe(
|
||||
map((msg) => {
|
||||
msg.Status = MessageStatus.DeliveryAck;
|
||||
return msg;
|
||||
}),
|
||||
mergeMap((msg) => this.processIncomingMessage(msg, true)),
|
||||
share(), // share it so we don't process twice in message.any
|
||||
);
|
||||
@@ -361,14 +410,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
const receipt$ = all$.pipe(onlyEvent(WhatsMeowEvent.RECEIPT));
|
||||
const messageAck$ = receipt$.pipe(
|
||||
mergeMap(this.receiptToMessageAck.bind(this)),
|
||||
// Create a composite key for deduplication
|
||||
groupBy((message) => `${message.id}-${message.ack}`),
|
||||
mergeMap((group$) =>
|
||||
group$.pipe(
|
||||
debounceTime(1000), // Wait 1 second for deduplication
|
||||
map((message) => message), // Pass the latest message after debounce
|
||||
),
|
||||
),
|
||||
DistinctAck(),
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_ACK).switch(messageAck$);
|
||||
|
||||
@@ -415,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> {
|
||||
@@ -583,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);
|
||||
@@ -703,15 +862,24 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
async sendSeen(request: SendSeenRequest) {
|
||||
const key = parseMessageIdSerialized(request.messageId);
|
||||
const req = new messages.MarkReadRequest({
|
||||
session: this.session,
|
||||
jid: key.remoteJid,
|
||||
messageId: key.id,
|
||||
sender: key.fromMe ? this.me.id : key.participant,
|
||||
});
|
||||
const response = await promisify(this.client.MarkRead)(req);
|
||||
response.toObject();
|
||||
const keys = ExtractMessageKeysForRead(request);
|
||||
if (keys.length === 0) {
|
||||
return;
|
||||
}
|
||||
const receipts = aggregateMessageKeysNotFromMe(keys);
|
||||
for (const receipt of receipts) {
|
||||
if (receipt.messageIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
const req = new messages.MarkReadRequest({
|
||||
session: this.session,
|
||||
jid: receipt.jid,
|
||||
messageIds: receipt.messageIds,
|
||||
sender: receipt.participant,
|
||||
});
|
||||
const response = await promisify(this.client.MarkRead)(req);
|
||||
response.toObject();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -933,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;
|
||||
@@ -993,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) {
|
||||
@@ -1233,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';
|
||||
@@ -1245,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) {
|
||||
@@ -1284,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({
|
||||
@@ -1301,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);
|
||||
@@ -1322,6 +1610,8 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
});
|
||||
}
|
||||
|
||||
const status =
|
||||
filter['filter.ack'] != null ? AckToStatus(filter['filter.ack']) : null;
|
||||
const request = new messages.GetMessagesRequest({
|
||||
session: this.session,
|
||||
filters: new messages.MessageFilters({
|
||||
@@ -1335,6 +1625,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
messages.OptionalUInt64,
|
||||
),
|
||||
fromMe: optional(filter['filter.fromMe'], messages.OptionalBool),
|
||||
status: optional(status, messages.OptionalUInt32),
|
||||
}),
|
||||
pagination: new messages.Pagination({
|
||||
limit: query.limit,
|
||||
@@ -1352,6 +1643,13 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
return result;
|
||||
}
|
||||
|
||||
public readChatMessages(
|
||||
chatId: string,
|
||||
request: ReadChatMessagesQuery,
|
||||
): Promise<ReadChatMessagesResponse> {
|
||||
return this.readChatMessagesWSImpl(chatId, request);
|
||||
}
|
||||
|
||||
public async getChatMessage(
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
@@ -1367,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
|
||||
//
|
||||
@@ -1375,14 +1786,21 @@ 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;
|
||||
// Ignore key distribution messages
|
||||
if (message.Message.senderKeyDistributionMessage) return;
|
||||
|
||||
const normalizedContent = normalizeMessageContent(message.Message);
|
||||
const hasSomeContent = !!getContentType(normalizedContent);
|
||||
if (!hasSomeContent) {
|
||||
// Ignore key distribution messages
|
||||
if (message?.Message?.senderKeyDistributionMessage) return;
|
||||
}
|
||||
|
||||
if (downloadMedia) {
|
||||
try {
|
||||
@@ -1405,11 +1823,11 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
const id = buildMessageId(message);
|
||||
const body = this.extractBody(message.Message);
|
||||
const replyTo = this.extractReplyTo(message.Message);
|
||||
let ack;
|
||||
if (message.Status) {
|
||||
ack = statusToAck(message.Status);
|
||||
} else {
|
||||
ack = message.Info.IsFromMe ? WAMessageAck.SERVER : WAMessageAck.DEVICE;
|
||||
let ack = statusToAck(message.Status);
|
||||
if (ack === WAMessageAck.ERROR) {
|
||||
// GOWS error because of how golang treats it as null
|
||||
// It'll be UNKNOWN instead of ERROR
|
||||
ack = null;
|
||||
}
|
||||
const mediaContent = extractMediaContent(message.Message);
|
||||
const source = this.getSourceDeviceByMsg(message);
|
||||
@@ -1436,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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import makeWASocket, {
|
||||
Browsers,
|
||||
Chat,
|
||||
Contact,
|
||||
decryptPollVote,
|
||||
DisconnectReason,
|
||||
downloadMediaMessage,
|
||||
extractMessageContent,
|
||||
@@ -36,17 +37,16 @@ import {
|
||||
LabelAssociationType,
|
||||
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
|
||||
import { MessageUserReceiptUpdate } from '@adiwajshing/baileys/lib/Types/Message';
|
||||
import { ILogger } from '@adiwajshing/baileys/lib/Utils/logger';
|
||||
import {
|
||||
isJidBroadcast,
|
||||
isLidUser,
|
||||
} from '@adiwajshing/baileys/lib/WABinary/jid-utils';
|
||||
import { Logger as BaileysLogger } from '@adiwajshing/baileys/node_modules/pino';
|
||||
import { UnprocessableEntityException } from '@nestjs/common';
|
||||
import {
|
||||
ensureSuffix,
|
||||
getChannelInviteLink,
|
||||
getPublicUrlFromDirectPath,
|
||||
isNewsletter,
|
||||
WhatsappSession,
|
||||
} from '@waha/core/abc/session.abc';
|
||||
import {
|
||||
@@ -66,6 +66,11 @@ import { toVcard } from '@waha/core/helpers';
|
||||
import { createAgentProxy } from '@waha/core/helpers.proxy';
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { QR } from '@waha/core/QR';
|
||||
import { AckToStatus, StatusToAck } from '@waha/core/utils/acks';
|
||||
import { ExtractMessageKeysForRead } from '@waha/core/utils/convertors';
|
||||
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
|
||||
import { isJidNewsletter, toJID } from '@waha/core/utils/jids';
|
||||
import { DistinctAck } from '@waha/core/utils/reactive';
|
||||
import { flipObject, splitAt } from '@waha/helpers';
|
||||
import { PairingCodeResponse } from '@waha/structures/auth.dto';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
@@ -85,7 +90,10 @@ import {
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
OverviewFilter,
|
||||
PinDuration,
|
||||
ReadChatMessagesQuery,
|
||||
ReadChatMessagesResponse,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto';
|
||||
import {
|
||||
@@ -131,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 {
|
||||
@@ -174,13 +183,13 @@ import {
|
||||
share,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const promiseRetry = require('promise-retry');
|
||||
|
||||
import { INowebStore } from './store/INowebStore';
|
||||
import { NowebPersistentStore } from './store/NowebPersistentStore';
|
||||
import { NowebStorageFactoryCore } from './store/NowebStorageFactoryCore';
|
||||
import { ensureNumber, extractMediaContent } from './utils';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const promiseRetry = require('promise-retry');
|
||||
|
||||
export const BaileysEvents = {
|
||||
CONNECTION_UPDATE: 'connection.update',
|
||||
@@ -214,7 +223,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
private autoRestartJob: SinglePeriodicJobRunner;
|
||||
private msgRetryCounterCache: NodeCache;
|
||||
private placeholderResendCache: NodeCache;
|
||||
protected engineLogger: BaileysLogger;
|
||||
protected engineLogger: ILogger;
|
||||
|
||||
private authNOWEBStore: any;
|
||||
|
||||
@@ -242,7 +251,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
|
||||
this.engineLogger = this.loggerBuilder.child({
|
||||
name: 'NOWEBEngine',
|
||||
}) as unknown as BaileysLogger;
|
||||
}) as unknown as ILogger;
|
||||
|
||||
// Restart job if session failed
|
||||
this.startDelayedJob = new SingleDelayedJobRunner(
|
||||
@@ -378,7 +387,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
|
||||
resubscribeToKnownPresences() {
|
||||
for (const jid in this.store.presences) {
|
||||
this.sock.presenceSubscribe(jid);
|
||||
this.subscribePresence(jid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,6 +400,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
this.sock = await this.makeSocket();
|
||||
|
||||
this.issueMessageUpdateOnEdits();
|
||||
this.issueMessageUpdateOnPoll();
|
||||
this.fixMessageUpsertStatus();
|
||||
this.issuePresenceUpdateOnMessageUpsert();
|
||||
if (this.isDebugEnabled()) {
|
||||
this.listenEngineEventsInDebugMode();
|
||||
@@ -458,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('');
|
||||
@@ -560,6 +574,17 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
await this.store?.close();
|
||||
}
|
||||
|
||||
private fixMessageUpsertStatus() {
|
||||
// If no status - set it to WAMessageAck.DEVICE
|
||||
this.sock.ev.on('messages.upsert', ({ messages }) => {
|
||||
for (const message of messages) {
|
||||
if (message.status == null) {
|
||||
message.status = AckToStatus(WAMessageAck.DEVICE);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private issueMessageUpdateOnEdits() {
|
||||
// Remove it after it's been merged
|
||||
// https://github.com/WhiskeySockets/Baileys/pull/855/
|
||||
@@ -586,6 +611,67 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
});
|
||||
}
|
||||
|
||||
private issueMessageUpdateOnPoll() {
|
||||
// Fix for https://github.com/devlikeapro/waha/issues/960
|
||||
this.sock.ev.on('messages.upsert', async ({ messages }) => {
|
||||
const meId = this.getSessionMeInfo().id;
|
||||
if (!meId) {
|
||||
return;
|
||||
}
|
||||
for (const message of messages) {
|
||||
const content = normalizeMessageContent(message.message);
|
||||
if (!content?.pollUpdateMessage) {
|
||||
continue;
|
||||
}
|
||||
const creationMsgKey = content.pollUpdateMessage.pollCreationMessageKey;
|
||||
// we need to fetch the poll creation message to get the poll enc key
|
||||
const pollMsg = await this.getMessage(creationMsgKey);
|
||||
if (!pollMsg) {
|
||||
this.logger.warn(
|
||||
{ creationMsgKey },
|
||||
'poll creation message not found, cannot decrypt update',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const meIdNormalised = jidNormalizedUser(meId);
|
||||
const pollCreatorJid = getKeyAuthor(creationMsgKey, meIdNormalised);
|
||||
const voterJid = getKeyAuthor(message.key, meIdNormalised);
|
||||
const pollEncKey = pollMsg.messageContextInfo?.messageSecret;
|
||||
|
||||
try {
|
||||
const voteMsg = decryptPollVote(content.pollUpdateMessage.vote, {
|
||||
pollEncKey,
|
||||
pollCreatorJid,
|
||||
pollMsgId: creationMsgKey.id,
|
||||
voterJid,
|
||||
});
|
||||
this.sock.ev.emit('messages.update', [
|
||||
{
|
||||
key: creationMsgKey,
|
||||
update: {
|
||||
pollUpdates: [
|
||||
{
|
||||
pollUpdateMessageKey: message.key,
|
||||
vote: voteMsg,
|
||||
senderTimestampMs: (
|
||||
content.pollUpdateMessage.senderTimestampMs as Long
|
||||
).toNumber(),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
{ err, creationMsgKey },
|
||||
'failed to decrypt poll vote',
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private issuePresenceUpdateOnMessageUpsert() {
|
||||
//
|
||||
// Fix for "typing" after sending a message
|
||||
@@ -893,16 +979,20 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
async sendSeen(request: SendSeenRequest) {
|
||||
const key = parseMessageIdSerialized(request.messageId);
|
||||
const participant = request.participant
|
||||
? toJID(this.ensureSuffix(request.participant))
|
||||
: undefined;
|
||||
const data = {
|
||||
remoteJid: key.remoteJid,
|
||||
id: key.id,
|
||||
participant: participant,
|
||||
};
|
||||
return this.sock.readMessages([data]);
|
||||
const keys = ExtractMessageKeysForRead(request);
|
||||
if (keys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send read
|
||||
await this.sock.readMessages(keys);
|
||||
|
||||
// Emit events for our reads
|
||||
const updates = keys.map((key) => ({
|
||||
key: key,
|
||||
update: { status: AckToStatus(WAMessageAck.READ) },
|
||||
}));
|
||||
this.sock?.ev.emit('messages.update', updates);
|
||||
}
|
||||
|
||||
async startTyping(request: ChatRequest) {
|
||||
@@ -937,6 +1027,13 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
return result;
|
||||
}
|
||||
|
||||
public readChatMessages(
|
||||
chatId: string,
|
||||
request: ReadChatMessagesQuery,
|
||||
): Promise<ReadChatMessagesResponse> {
|
||||
return this.readChatMessagesWSImpl(chatId, request);
|
||||
}
|
||||
|
||||
public async getChatMessage(
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
@@ -978,7 +1075,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
|
||||
async setReaction(request: MessageReactionRequest) {
|
||||
const key = parseMessageIdSerialized(request.messageId);
|
||||
if (isNewsletter(key.remoteJid)) {
|
||||
if (isJidNewsletter(key.remoteJid)) {
|
||||
let serverId = Number(key.id);
|
||||
if (!serverId) {
|
||||
const msg = await this.store.getMessageById(key.remoteJid, key.id);
|
||||
@@ -1034,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);
|
||||
|
||||
@@ -1247,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
|
||||
*/
|
||||
@@ -1381,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> {
|
||||
@@ -1680,7 +1824,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
// @ts-ignore
|
||||
filter(
|
||||
(message) =>
|
||||
message.message.protocolMessage?.type ===
|
||||
message.message?.protocolMessage?.type ===
|
||||
proto.Message.ProtocolMessage.Type.REVOKE,
|
||||
),
|
||||
mergeMap(async (message): Promise<WAMessageRevokedBody> => {
|
||||
@@ -1728,7 +1872,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
filter(isMine), // ack comes only for MY messages
|
||||
map(this.convertMessageReceiptUpdateToMessageAck.bind(this)),
|
||||
);
|
||||
const messageAck$ = merge(messageAckDirect$, messageAckGroups$);
|
||||
const messageAck$ = merge(messageAckDirect$, messageAckGroups$).pipe(
|
||||
DistinctAck(),
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_ACK).switch(messageAck$);
|
||||
|
||||
//
|
||||
@@ -1925,7 +2071,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
if (!message.message.reactionMessage) return null;
|
||||
|
||||
const id = buildMessageId(message.key);
|
||||
const fromToParticipant = getFromToParticipant(message);
|
||||
const fromToParticipant = getFromToParticipant(message.key);
|
||||
const reactionMessage = message.message.reactionMessage;
|
||||
const messageId = buildMessageId(reactionMessage.key);
|
||||
const source = this.getMessageSource(message.key.id);
|
||||
@@ -1957,17 +2103,22 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
if (message.message.call?.callKey) return;
|
||||
// Ignore revoke, we have a dedicated handler for that
|
||||
if (
|
||||
message.message.protocolMessage?.type ===
|
||||
message.message?.protocolMessage?.type ===
|
||||
proto.Message.ProtocolMessage.Type.REVOKE
|
||||
)
|
||||
return;
|
||||
if (
|
||||
message.message.protocolMessage?.type ===
|
||||
message.message?.protocolMessage?.type ===
|
||||
proto.Message.ProtocolMessage.Type.EPHEMERAL_SYNC_RESPONSE
|
||||
)
|
||||
return;
|
||||
// Ignore key distribution messages
|
||||
if (message.message.senderKeyDistributionMessage) return;
|
||||
|
||||
const normalizedContent = normalizeMessageContent(message.message);
|
||||
const hasSomeContent = !!getContentType(normalizedContent);
|
||||
if (!hasSomeContent) {
|
||||
// Ignore key distribution messages
|
||||
if (message?.message?.senderKeyDistributionMessage) return;
|
||||
}
|
||||
|
||||
if (downloadMedia) {
|
||||
try {
|
||||
@@ -1989,11 +2140,11 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
protected toWAMessage(message): Promise<WAMessage> {
|
||||
const fromToParticipant = getFromToParticipant(message);
|
||||
const fromToParticipant = getFromToParticipant(message.key);
|
||||
const id = buildMessageId(message.key);
|
||||
const body = this.extractBody(message.message);
|
||||
const replyTo = this.extractReplyTo(message.message);
|
||||
const ack = message.ack || message.status - 1;
|
||||
const ack = message.ack || StatusToAck(message.status);
|
||||
const mediaContent = extractMediaContent(message.message);
|
||||
const source = this.getMessageSource(message.key.id);
|
||||
return Promise.resolve({
|
||||
@@ -2080,9 +2231,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
|
||||
protected convertMessageUpdateToMessageAck(event): WAMessageAckBody {
|
||||
const message = event;
|
||||
const fromToParticipant = getFromToParticipant(message);
|
||||
const fromToParticipant = getFromToParticipant(message.key);
|
||||
const id = buildMessageId(message.key);
|
||||
const ack = message.update.status - 1;
|
||||
const ack = StatusToAck(message.update.status);
|
||||
const body: WAMessageAckBody = {
|
||||
id: id,
|
||||
from: toCusFormat(fromToParticipant.from),
|
||||
@@ -2096,8 +2247,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
protected convertMessageReceiptUpdateToMessageAck(event): WAMessageAckBody {
|
||||
const fromToParticipant = getFromToParticipant(event);
|
||||
const id = buildMessageId(event.key);
|
||||
const fromToParticipant = getFromToParticipant(event.key);
|
||||
|
||||
const receipt = event.receipt;
|
||||
let ack;
|
||||
@@ -2108,6 +2258,15 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
} else if (receipt.readTimestamp) {
|
||||
ack = WAMessageAck.READ;
|
||||
}
|
||||
|
||||
const key = { ...event.key };
|
||||
if (key.fromMe) {
|
||||
key.participant = this.getSessionMeInfo()?.id;
|
||||
} else {
|
||||
key.participant = event.receipt.userJid;
|
||||
}
|
||||
const id = buildMessageId(key);
|
||||
|
||||
const body: WAMessageAckBody = {
|
||||
id: id,
|
||||
from: toCusFormat(fromToParticipant.from),
|
||||
@@ -2116,6 +2275,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
fromMe: event.key.fromMe,
|
||||
ack: ack,
|
||||
ackName: WAMessageAck[ack] || ACK_UNKNOWN,
|
||||
_data: event,
|
||||
};
|
||||
return body;
|
||||
}
|
||||
@@ -2291,7 +2451,7 @@ function hasPath(url: string) {
|
||||
}
|
||||
|
||||
export class NOWEBEngineMediaProcessor implements IMediaEngineProcessor<any> {
|
||||
private readonly logger: BaileysLogger;
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(
|
||||
public session: WhatsappSessionNoWebCore,
|
||||
@@ -2299,7 +2459,7 @@ export class NOWEBEngineMediaProcessor implements IMediaEngineProcessor<any> {
|
||||
) {
|
||||
this.logger = loggerBuilder.child({
|
||||
name: NOWEBEngineMediaProcessor.name,
|
||||
}) as unknown as BaileysLogger;
|
||||
}) as unknown as ILogger;
|
||||
}
|
||||
|
||||
hasMedia(message: any): boolean {
|
||||
@@ -2366,7 +2526,7 @@ export function toCusFormat(remoteJid) {
|
||||
if (isLidUser(remoteJid)) {
|
||||
return remoteJid;
|
||||
}
|
||||
if (isNewsletter(remoteJid)) {
|
||||
if (isJidNewsletter(remoteJid)) {
|
||||
return remoteJid;
|
||||
}
|
||||
if (!remoteJid) {
|
||||
@@ -2383,27 +2543,6 @@ export function toCusFormat(remoteJid) {
|
||||
|
||||
export const ALL_JID = 'all@s.whatsapp.net';
|
||||
|
||||
/**
|
||||
* Convert from 11111111111@c.us to 11111111111@s.whatsapp.net
|
||||
* @param chatId
|
||||
*/
|
||||
export function toJID(chatId) {
|
||||
if (isJidGroup(chatId)) {
|
||||
return chatId;
|
||||
}
|
||||
if (isJidBroadcast(chatId)) {
|
||||
return chatId;
|
||||
}
|
||||
if (isNewsletter(chatId)) {
|
||||
return chatId;
|
||||
}
|
||||
if (isLidUser(chatId)) {
|
||||
return chatId;
|
||||
}
|
||||
const number = chatId.split('@')[0];
|
||||
return number + '@s.whatsapp.net';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build WAHA message id from engine one
|
||||
* {id: "AAA", remoteJid: "11111111111@s.whatsapp.net", "fromMe": false}
|
||||
@@ -2418,38 +2557,6 @@ function buildMessageId({ id, remoteJid, fromMe, participant }: WAMessageKey) {
|
||||
return parts.join('_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse message id from WAHA to engine
|
||||
* false_11111111111@c.us_AAA
|
||||
* {id: "AAA", remoteJid: "11111111111@s.whatsapp.net", "fromMe": false}
|
||||
*/
|
||||
export function parseMessageIdSerialized(
|
||||
messageId: string,
|
||||
soft: boolean = false,
|
||||
): WAMessageKey {
|
||||
if (!messageId.includes('_') && soft) {
|
||||
return { id: messageId };
|
||||
}
|
||||
|
||||
const parts = messageId.split('_');
|
||||
if (parts.length != 3 && parts.length != 4) {
|
||||
throw new Error(
|
||||
'Message id be in format false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA[_participant]',
|
||||
);
|
||||
}
|
||||
const fromMe = parts[0] == 'true';
|
||||
const chatId = parts[1];
|
||||
const remoteJid = toJID(chatId);
|
||||
const id = parts[2];
|
||||
const participant = parts[3] ? toJID(parts[3]) : undefined;
|
||||
return {
|
||||
fromMe: fromMe,
|
||||
id: id,
|
||||
remoteJid: remoteJid,
|
||||
participant: participant,
|
||||
};
|
||||
}
|
||||
|
||||
function getId(object) {
|
||||
return object.id;
|
||||
}
|
||||
@@ -2466,15 +2573,15 @@ function isAckUpdateMessageEvent(event) {
|
||||
return event?.update.status != null;
|
||||
}
|
||||
|
||||
function getFromToParticipant(message) {
|
||||
const isGroupMessage = Boolean(message.key.participant);
|
||||
export function getFromToParticipant(key) {
|
||||
const isGroupMessage = Boolean(key.participant);
|
||||
let participant: string;
|
||||
let to: string;
|
||||
if (isGroupMessage) {
|
||||
participant = message.key.participant;
|
||||
to = message.key.remoteJid;
|
||||
participant = key.participant;
|
||||
to = key.remoteJid;
|
||||
}
|
||||
const from = message.key.remoteJid;
|
||||
const from = key.remoteJid;
|
||||
return {
|
||||
from: from,
|
||||
to: to,
|
||||
@@ -2506,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>;
|
||||
@@ -18,4 +20,6 @@ export interface IChatRepository {
|
||||
deleteById(id: string): Promise<void>;
|
||||
|
||||
save(chat: Chat): Promise<void>;
|
||||
|
||||
upsertMany(chats: Chat[]): Promise<void>;
|
||||
}
|
||||
@@ -11,4 +11,8 @@ export interface IContactRepository {
|
||||
deleteById(id: string): Promise<void>;
|
||||
|
||||
save(contact: Contact): Promise<void>;
|
||||
|
||||
upsertMany(contacts: Contact[]): Promise<void>;
|
||||
|
||||
getEntitiesByIds(ids: string[]): Promise<Map<string, Contact | 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;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -2,16 +2,20 @@ import makeWASocket, {
|
||||
Chat,
|
||||
Contact,
|
||||
GroupMetadata,
|
||||
makeInMemoryStore,
|
||||
proto,
|
||||
} from '@adiwajshing/baileys';
|
||||
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';
|
||||
import makeInMemoryStore from './memory/make-in-memory-store';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const logger = require('pino')();
|
||||
@@ -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,11 +234,31 @@ 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(
|
||||
`got update for non-existent message. update: '${JSON.stringify(
|
||||
update,
|
||||
)}'`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const fields = { ...update.update };
|
||||
// check if fields has only "status" field
|
||||
const onlyStatusField =
|
||||
Object.keys(fields).length === 1 &&
|
||||
'status' in fields &&
|
||||
fields.status !== null;
|
||||
if (onlyStatusField) {
|
||||
// if so, check the message don't have a newer status
|
||||
if (message.status >= fields.status) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// It can overwrite the key, so we need to delete it
|
||||
delete fields['key'];
|
||||
Object.assign(message, fields);
|
||||
@@ -226,8 +288,8 @@ export class NowebPersistentStore implements INowebStore {
|
||||
for (const chat of chats) {
|
||||
delete chat['messages'];
|
||||
chat.conversationTimestamp = toNumber(chat.conversationTimestamp) || null;
|
||||
await this.chatRepo.save(chat);
|
||||
}
|
||||
await this.chatRepo.upsertMany(chats);
|
||||
this.logger.info(`store sync - '${chats.length}' synced chats`);
|
||||
}
|
||||
|
||||
@@ -333,15 +395,19 @@ export class NowebPersistentStore implements INowebStore {
|
||||
}
|
||||
|
||||
private async onContactsUpsert(contacts: Contact[]) {
|
||||
const upserts = [];
|
||||
const ids = contacts.map((c) => c.id);
|
||||
const contactById = await this.contactRepo.getEntitiesByIds(ids);
|
||||
for (const update of contacts) {
|
||||
const contact = await this.contactRepo.getById(update.id);
|
||||
const contact = contactById.get(update.id) || {};
|
||||
// remove undefined from data
|
||||
Object.keys(update).forEach(
|
||||
(key) => update[key] === undefined && delete update[key],
|
||||
);
|
||||
const result = { ...(contact || {}), ...update };
|
||||
await this.contactRepo.save(result);
|
||||
const result = { ...contact, ...update };
|
||||
upserts.push(result);
|
||||
}
|
||||
await this.contactRepo.upsertMany(upserts);
|
||||
}
|
||||
|
||||
private async onContactUpdate(updates: Partial<Contact>[]) {
|
||||
@@ -461,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> {
|
||||
@@ -538,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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import type {
|
||||
BaileysEventEmitter,
|
||||
Chat,
|
||||
ConnectionState,
|
||||
Contact,
|
||||
GroupMetadata,
|
||||
PresenceData,
|
||||
WAMessage,
|
||||
WAMessageCursor,
|
||||
WAMessageKey,
|
||||
} from '@adiwajshing/baileys/lib/Types';
|
||||
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import {
|
||||
LabelAssociation,
|
||||
LabelAssociationType,
|
||||
MessageLabelAssociation,
|
||||
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
|
||||
import {
|
||||
md5,
|
||||
toNumber,
|
||||
updateMessageWithReaction,
|
||||
updateMessageWithReceipt,
|
||||
} from '@adiwajshing/baileys/lib/Utils';
|
||||
import {
|
||||
jidDecode,
|
||||
jidNormalizedUser,
|
||||
} from '@adiwajshing/baileys/lib/WABinary';
|
||||
import { proto } from '@adiwajshing/baileys/WAProto';
|
||||
import type { Comparable } from '@adiwajshing/keyed-db/lib/Types';
|
||||
import type { Logger } from 'pino';
|
||||
|
||||
import makeOrderedDictionary from './make-ordered-dictionary';
|
||||
import { ObjectRepository } from './object-repository';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const KeyedDB = require('@adiwajshing/keyed-db').default;
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { writeFileSync } = require('fs');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { readFileSync, existsSync } = require('fs');
|
||||
|
||||
type WASocket = ReturnType<any>;
|
||||
|
||||
export const waChatKey = (pin: boolean) => ({
|
||||
key: (c: Chat) =>
|
||||
(pin ? (c.pinned ? '1' : '0') : '') +
|
||||
(c.archived ? '0' : '1') +
|
||||
(c.conversationTimestamp
|
||||
? c.conversationTimestamp.toString(16).padStart(8, '0')
|
||||
: '') +
|
||||
c.id,
|
||||
compare: (k1: string, k2: string) => k2.localeCompare(k1),
|
||||
});
|
||||
|
||||
export const waMessageID = (m: WAMessage) => m.key.id || '';
|
||||
|
||||
export const waLabelAssociationKey: Comparable<LabelAssociation, string> = {
|
||||
key: (la: LabelAssociation) =>
|
||||
la.type === LabelAssociationType.Chat
|
||||
? la.chatId + la.labelId
|
||||
: la.chatId + la.messageId + la.labelId,
|
||||
compare: (k1: string, k2: string) => k2.localeCompare(k1),
|
||||
};
|
||||
|
||||
export type BaileysInMemoryStoreConfig = {
|
||||
chatKey?: Comparable<Chat, string>;
|
||||
labelAssociationKey?: Comparable<LabelAssociation, string>;
|
||||
logger: Logger;
|
||||
socket?: WASocket;
|
||||
};
|
||||
|
||||
const makeMessagesDictionary = () => makeOrderedDictionary(waMessageID);
|
||||
|
||||
export default (config: BaileysInMemoryStoreConfig) => {
|
||||
const socket = config.socket;
|
||||
const chatKey = config.chatKey || waChatKey(true);
|
||||
const labelAssociationKey =
|
||||
config.labelAssociationKey || waLabelAssociationKey;
|
||||
const logger: Logger = config.logger;
|
||||
|
||||
const chats = new KeyedDB(chatKey, (c) => c.id) as any;
|
||||
const messages: { [_: string]: ReturnType<typeof makeMessagesDictionary> } =
|
||||
{};
|
||||
const contacts: { [_: string]: Contact } = {};
|
||||
const groupMetadata: { [_: string]: GroupMetadata } = {};
|
||||
let presences: { [id: string]: { [participant: string]: PresenceData } } = {};
|
||||
const state: ConnectionState = { connection: 'close' };
|
||||
const labels = new ObjectRepository<Label>();
|
||||
const labelAssociations = new KeyedDB(
|
||||
labelAssociationKey,
|
||||
labelAssociationKey.key,
|
||||
) as any;
|
||||
|
||||
const assertMessageList = (jid: string) => {
|
||||
if (!messages[jid]) {
|
||||
messages[jid] = makeMessagesDictionary();
|
||||
}
|
||||
|
||||
return messages[jid];
|
||||
};
|
||||
|
||||
const contactsUpsert = (newContacts: Contact[]) => {
|
||||
const oldContacts = new Set(Object.keys(contacts));
|
||||
for (const contact of newContacts) {
|
||||
oldContacts.delete(contact.id);
|
||||
contacts[contact.id] = Object.assign(contacts[contact.id] || {}, contact);
|
||||
}
|
||||
|
||||
return oldContacts;
|
||||
};
|
||||
|
||||
const labelsUpsert = (newLabels: Label[]) => {
|
||||
for (const label of newLabels) {
|
||||
labels.upsertById(label.id, label);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* binds to a BaileysEventEmitter.
|
||||
* It listens to all events and constructs a state that you can query accurate data from.
|
||||
* Eg. can use the store to fetch chats, contacts, messages etc.
|
||||
* @param ev typically the event emitter from the socket connection
|
||||
*/
|
||||
const bind = (ev: BaileysEventEmitter) => {
|
||||
ev.on('connection.update', (update) => {
|
||||
Object.assign(state, update);
|
||||
});
|
||||
|
||||
ev.on(
|
||||
'messaging-history.set',
|
||||
({
|
||||
chats: newChats,
|
||||
contacts: newContacts,
|
||||
messages: newMessages,
|
||||
isLatest,
|
||||
syncType,
|
||||
}) => {
|
||||
if (syncType === proto.HistorySync.HistorySyncType.ON_DEMAND) {
|
||||
return; // FOR NOW,
|
||||
//TODO: HANDLE
|
||||
}
|
||||
|
||||
if (isLatest) {
|
||||
chats.clear();
|
||||
|
||||
for (const id in messages) {
|
||||
delete messages[id];
|
||||
}
|
||||
}
|
||||
|
||||
const chatsAdded = chats.insertIfAbsent(...newChats).length;
|
||||
logger.debug({ chatsAdded }, 'synced chats');
|
||||
|
||||
const oldContacts = contactsUpsert(newContacts);
|
||||
if (isLatest) {
|
||||
for (const jid of oldContacts) {
|
||||
delete contacts[jid];
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
{ deletedContacts: isLatest ? oldContacts.size : 0, newContacts },
|
||||
'synced contacts',
|
||||
);
|
||||
|
||||
for (const msg of newMessages) {
|
||||
const jid = msg.key.remoteJid!;
|
||||
const list = assertMessageList(jid);
|
||||
list.upsert(msg, 'prepend');
|
||||
}
|
||||
|
||||
logger.debug({ messages: newMessages.length }, 'synced messages');
|
||||
},
|
||||
);
|
||||
|
||||
ev.on('contacts.upsert', (contacts) => {
|
||||
contactsUpsert(contacts);
|
||||
});
|
||||
|
||||
ev.on('contacts.update', async (updates) => {
|
||||
for (const update of updates) {
|
||||
let contact: Contact;
|
||||
if (contacts[update.id!]) {
|
||||
contact = contacts[update.id!];
|
||||
} else {
|
||||
const contactHashes = await Promise.all(
|
||||
Object.keys(contacts).map(async (contactId) => {
|
||||
const { user } = jidDecode(contactId)!;
|
||||
return [
|
||||
contactId,
|
||||
(await md5(Buffer.from(user + 'WA_ADD_NOTIF', 'utf8')))
|
||||
.toString('base64')
|
||||
.slice(0, 3),
|
||||
];
|
||||
}),
|
||||
);
|
||||
contact =
|
||||
contacts[contactHashes.find(([, b]) => b === update.id)?.[0] || '']; // find contact by attrs.hash, when user is not saved as a contact
|
||||
}
|
||||
|
||||
if (contact) {
|
||||
if (update.imgUrl === 'changed') {
|
||||
contact.imgUrl = socket
|
||||
? await socket?.profilePictureUrl(contact.id)
|
||||
: undefined;
|
||||
} else if (update.imgUrl === 'removed') {
|
||||
delete contact.imgUrl;
|
||||
}
|
||||
} else {
|
||||
return logger.debug(
|
||||
{ update },
|
||||
'got update for non-existant contact',
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(contacts[contact.id], contact);
|
||||
}
|
||||
});
|
||||
ev.on('chats.upsert', (newChats) => {
|
||||
chats.upsert(...newChats);
|
||||
});
|
||||
ev.on('chats.update', (updates) => {
|
||||
for (let update of updates) {
|
||||
const result = chats.update(update.id!, (chat) => {
|
||||
if (update.unreadCount! > 0) {
|
||||
update = { ...update };
|
||||
update.unreadCount = (chat.unreadCount || 0) + update.unreadCount!;
|
||||
}
|
||||
|
||||
Object.assign(chat, update);
|
||||
});
|
||||
if (!result) {
|
||||
logger.debug({ update }, 'got update for non-existant chat');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ev.on('labels.edit', (label: Label) => {
|
||||
if (label.deleted) {
|
||||
return labels.deleteById(label.id);
|
||||
}
|
||||
|
||||
// WhatsApp can store only up to 20 labels
|
||||
if (labels.count() < 20) {
|
||||
return labels.upsertById(label.id, label);
|
||||
}
|
||||
|
||||
logger.error('Labels count exceed');
|
||||
});
|
||||
|
||||
ev.on('labels.association', ({ type, association }) => {
|
||||
switch (type) {
|
||||
case 'add':
|
||||
labelAssociations.upsert(association);
|
||||
break;
|
||||
case 'remove':
|
||||
labelAssociations.delete(association);
|
||||
break;
|
||||
default:
|
||||
console.error(`unknown operation type [${type}]`);
|
||||
}
|
||||
});
|
||||
|
||||
ev.on('presence.update', ({ id, presences: update }) => {
|
||||
presences[id] = presences[id] || {};
|
||||
Object.assign(presences[id], update);
|
||||
});
|
||||
ev.on('chats.delete', (deletions) => {
|
||||
for (const item of deletions) {
|
||||
if (chats.get(item)) {
|
||||
chats.deleteById(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
ev.on('messages.upsert', ({ messages: newMessages, type }) => {
|
||||
switch (type) {
|
||||
case 'append':
|
||||
case 'notify':
|
||||
for (const msg of newMessages) {
|
||||
const jid = jidNormalizedUser(msg.key.remoteJid!);
|
||||
const list = assertMessageList(jid);
|
||||
list.upsert(msg, 'append');
|
||||
|
||||
if (type === 'notify' && !chats.get(jid)) {
|
||||
ev.emit('chats.upsert', [
|
||||
{
|
||||
id: jid,
|
||||
conversationTimestamp: toNumber(msg.messageTimestamp),
|
||||
unreadCount: 1,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
});
|
||||
ev.on('messages.update', (updates) => {
|
||||
for (const { update, key } of updates) {
|
||||
const list = assertMessageList(jidNormalizedUser(key.remoteJid!));
|
||||
if (update?.status) {
|
||||
const listStatus = list.get(key.id!)?.status;
|
||||
if (listStatus && update?.status <= listStatus) {
|
||||
logger.debug(
|
||||
{ update, storedStatus: listStatus },
|
||||
'status stored newer then update',
|
||||
);
|
||||
delete update.status;
|
||||
logger.debug({ update }, 'new update object');
|
||||
}
|
||||
}
|
||||
|
||||
const result = list.updateAssign(key.id!, update);
|
||||
if (!result) {
|
||||
logger.debug({ update }, 'got update for non-existent message');
|
||||
}
|
||||
}
|
||||
});
|
||||
ev.on('messages.delete', (item) => {
|
||||
if ('all' in item) {
|
||||
const list = messages[item.jid];
|
||||
list?.clear();
|
||||
} else {
|
||||
const jid = item.keys[0].remoteJid!;
|
||||
const list = messages[jid];
|
||||
if (list) {
|
||||
const idSet = new Set(item.keys.map((k) => k.id));
|
||||
list.filter((m) => !idSet.has(m.key.id));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ev.on('groups.update', (updates) => {
|
||||
for (const update of updates) {
|
||||
const id = update.id!;
|
||||
if (groupMetadata[id]) {
|
||||
Object.assign(groupMetadata[id], update);
|
||||
} else {
|
||||
logger.debug(
|
||||
{ update },
|
||||
'got update for non-existant group metadata',
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ev.on('group-participants.update', ({ id, participants, action }) => {
|
||||
const metadata = groupMetadata[id];
|
||||
if (metadata) {
|
||||
switch (action) {
|
||||
case 'add':
|
||||
metadata.participants.push(
|
||||
...participants.map((id) => ({
|
||||
id,
|
||||
isAdmin: false,
|
||||
isSuperAdmin: false,
|
||||
})),
|
||||
);
|
||||
break;
|
||||
case 'demote':
|
||||
case 'promote':
|
||||
for (const participant of metadata.participants) {
|
||||
if (participants.includes(participant.id)) {
|
||||
participant.isAdmin = action === 'promote';
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case 'remove':
|
||||
metadata.participants = metadata.participants.filter(
|
||||
(p) => !participants.includes(p.id),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ev.on('message-receipt.update', (updates) => {
|
||||
for (const { key, receipt } of updates) {
|
||||
const obj = messages[key.remoteJid!];
|
||||
const msg = obj?.get(key.id!);
|
||||
if (msg) {
|
||||
updateMessageWithReceipt(msg, receipt);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ev.on('messages.reaction', (reactions) => {
|
||||
for (const { key, reaction } of reactions) {
|
||||
const obj = messages[key.remoteJid!];
|
||||
const msg = obj?.get(key.id!);
|
||||
if (msg) {
|
||||
updateMessageWithReaction(msg, reaction);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const toJSON = () => ({
|
||||
chats,
|
||||
contacts,
|
||||
messages,
|
||||
labels,
|
||||
labelAssociations,
|
||||
});
|
||||
|
||||
const fromJSON = (json: {
|
||||
chats: Chat[];
|
||||
contacts: { [id: string]: Contact };
|
||||
messages: { [id: string]: WAMessage[] };
|
||||
labels: { [labelId: string]: Label };
|
||||
labelAssociations: LabelAssociation[];
|
||||
}) => {
|
||||
chats.upsert(...json.chats);
|
||||
labelAssociations.upsert(...(json.labelAssociations || []));
|
||||
contactsUpsert(Object.values(json.contacts));
|
||||
labelsUpsert(Object.values(json.labels || {}));
|
||||
for (const jid in json.messages) {
|
||||
const list = assertMessageList(jid);
|
||||
for (const msg of json.messages[jid]) {
|
||||
list.upsert(proto.WebMessageInfo.fromObject(msg), 'append');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
chats,
|
||||
contacts,
|
||||
messages,
|
||||
groupMetadata,
|
||||
state,
|
||||
presences,
|
||||
setPresences: (obj: any) => {
|
||||
presences = obj;
|
||||
},
|
||||
labels,
|
||||
labelAssociations,
|
||||
bind,
|
||||
/** loads messages from the store, if not found -- uses the legacy connection */
|
||||
loadMessages: async (
|
||||
jid: string,
|
||||
count: number,
|
||||
cursor: WAMessageCursor,
|
||||
) => {
|
||||
const list = assertMessageList(jid);
|
||||
const mode = !cursor || 'before' in cursor ? 'before' : 'after';
|
||||
const cursorKey = !!cursor
|
||||
? 'before' in cursor
|
||||
? cursor.before
|
||||
: cursor.after
|
||||
: undefined;
|
||||
const cursorValue = cursorKey ? list.get(cursorKey.id!) : undefined;
|
||||
|
||||
let messages: WAMessage[];
|
||||
if (list && mode === 'before' && (!cursorKey || cursorValue)) {
|
||||
if (cursorValue) {
|
||||
const msgIdx = list.array.findIndex(
|
||||
(m) => m.key.id === cursorKey?.id,
|
||||
);
|
||||
messages = list.array.slice(0, msgIdx);
|
||||
} else {
|
||||
messages = list.array;
|
||||
}
|
||||
|
||||
const diff = count - messages.length;
|
||||
if (diff < 0) {
|
||||
messages = messages.slice(-count); // get the last X messages
|
||||
}
|
||||
} else {
|
||||
messages = [];
|
||||
}
|
||||
|
||||
return messages;
|
||||
},
|
||||
/**
|
||||
* Get all available labels for profile
|
||||
*
|
||||
* Keep in mind that the list is formed from predefined tags and tags
|
||||
* that were "caught" during their editing.
|
||||
*/
|
||||
getLabels: () => {
|
||||
return labels;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get labels for chat
|
||||
*
|
||||
* @returns Label IDs
|
||||
**/
|
||||
getChatLabels: (chatId: string) => {
|
||||
return labelAssociations.filter((la) => la.chatId === chatId).all();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get labels for message
|
||||
*
|
||||
* @returns Label IDs
|
||||
**/
|
||||
getMessageLabels: (messageId: string) => {
|
||||
const associations = labelAssociations
|
||||
.filter((la: MessageLabelAssociation) => la.messageId === messageId)
|
||||
.all();
|
||||
|
||||
return associations.map(({ labelId }) => labelId);
|
||||
},
|
||||
loadMessage: async (jid: string, id: string) => messages[jid]?.get(id),
|
||||
mostRecentMessage: async (jid: string) => {
|
||||
const message: WAMessage | undefined = messages[jid]?.array.slice(-1)[0];
|
||||
return message;
|
||||
},
|
||||
fetchImageUrl: async (jid: string, sock: WASocket | undefined) => {
|
||||
const contact = contacts[jid];
|
||||
if (!contact) {
|
||||
return sock?.profilePictureUrl(jid);
|
||||
}
|
||||
|
||||
if (typeof contact.imgUrl === 'undefined') {
|
||||
contact.imgUrl = await sock?.profilePictureUrl(jid);
|
||||
}
|
||||
|
||||
return contact.imgUrl;
|
||||
},
|
||||
fetchGroupMetadata: async (jid: string, sock: WASocket | undefined) => {
|
||||
if (!groupMetadata[jid]) {
|
||||
const metadata = await sock?.groupMetadata(jid);
|
||||
if (metadata) {
|
||||
groupMetadata[jid] = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
return groupMetadata[jid];
|
||||
},
|
||||
// fetchBroadcastListInfo: async(jid: string, sock: WASocket | undefined) => {
|
||||
// if(!groupMetadata[jid]) {
|
||||
// const metadata = await sock?.getBroadcastListInfo(jid)
|
||||
// if(metadata) {
|
||||
// groupMetadata[jid] = metadata
|
||||
// }
|
||||
// }
|
||||
|
||||
// return groupMetadata[jid]
|
||||
// },
|
||||
fetchMessageReceipts: async ({ remoteJid, id }: WAMessageKey) => {
|
||||
const list = messages[remoteJid!];
|
||||
const msg = list?.get(id!);
|
||||
return msg?.userReceipt;
|
||||
},
|
||||
toJSON,
|
||||
fromJSON,
|
||||
writeToFile: (path: string) => {
|
||||
writeFileSync(path, JSON.stringify(toJSON()));
|
||||
},
|
||||
readFromFile: (path: string) => {
|
||||
if (existsSync(path)) {
|
||||
logger.debug({ path }, 'reading from file');
|
||||
const jsonStr = readFileSync(path, { encoding: 'utf-8' });
|
||||
const json = JSON.parse(jsonStr);
|
||||
fromJSON(json);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
function makeOrderedDictionary<T>(idGetter: (item: T) => string) {
|
||||
const array: T[] = [];
|
||||
const dict: { [_: string]: T } = {};
|
||||
|
||||
const get = (id: string): T | undefined => dict[id];
|
||||
|
||||
const update = (item: T) => {
|
||||
const id = idGetter(item);
|
||||
const idx = array.findIndex((i) => idGetter(i) === id);
|
||||
if (idx >= 0) {
|
||||
array[idx] = item;
|
||||
dict[id] = item;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const upsert = (item: T, mode: 'append' | 'prepend') => {
|
||||
const id = idGetter(item);
|
||||
if (get(id)) {
|
||||
update(item);
|
||||
} else {
|
||||
if (mode === 'append') {
|
||||
array.push(item);
|
||||
} else {
|
||||
array.splice(0, 0, item);
|
||||
}
|
||||
|
||||
dict[id] = item;
|
||||
}
|
||||
};
|
||||
|
||||
const remove = (item: T) => {
|
||||
const id = idGetter(item);
|
||||
const idx = array.findIndex((i) => idGetter(i) === id);
|
||||
if (idx >= 0) {
|
||||
array.splice(idx, 1);
|
||||
delete dict[id];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
array,
|
||||
get,
|
||||
upsert,
|
||||
update,
|
||||
remove,
|
||||
updateAssign: (id: string, update: Partial<T>) => {
|
||||
const item = get(id);
|
||||
if (item) {
|
||||
Object.assign(item, update);
|
||||
delete dict[id];
|
||||
dict[idGetter(item)] = item;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
clear: () => {
|
||||
array.splice(0, array.length);
|
||||
for (const key of Object.keys(dict)) {
|
||||
delete dict[key];
|
||||
}
|
||||
},
|
||||
filter: (contain: (item: T) => boolean) => {
|
||||
let i = 0;
|
||||
while (i < array.length) {
|
||||
if (!contain(array[i])) {
|
||||
delete dict[idGetter(array[i])];
|
||||
array.splice(i, 1);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
toJSON: () => array,
|
||||
fromJSON: (newItems: T[]) => {
|
||||
array.splice(0, array.length, ...newItems);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default makeOrderedDictionary;
|
||||
@@ -0,0 +1,32 @@
|
||||
export class ObjectRepository<T extends any> {
|
||||
readonly entityMap: Map<string, T>;
|
||||
|
||||
constructor(entities: Record<string, T> = {}) {
|
||||
this.entityMap = new Map(Object.entries(entities));
|
||||
}
|
||||
|
||||
findById(id: string) {
|
||||
return this.entityMap.get(id);
|
||||
}
|
||||
|
||||
findAll() {
|
||||
return Array.from(this.entityMap.values());
|
||||
}
|
||||
|
||||
upsertById(id: string, entity: T) {
|
||||
// @ts-ignore
|
||||
return this.entityMap.set(id, { ...entity });
|
||||
}
|
||||
|
||||
deleteById(id: string) {
|
||||
return this.entityMap.delete(id);
|
||||
}
|
||||
|
||||
count() {
|
||||
return this.entityMap.size;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.findAll();
|
||||
}
|
||||
}
|
||||
@@ -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,5 +1,6 @@
|
||||
import { ALL_JID } from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
|
||||
import { AckToStatus } from '@waha/core/utils/acks';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
@@ -35,14 +36,26 @@ export class SqlMessagesMethods {
|
||||
}
|
||||
if (filter['filter.fromMe'] != null) {
|
||||
// filter by data json inside
|
||||
const sql = this.repository.filterJson('data', 'key.fromMe');
|
||||
query = query.whereRaw(sql, [filter['filter.fromMe'] ? 'true' : 'false']);
|
||||
const [sql, value] = this.repository.filterJson(
|
||||
'data',
|
||||
'key.fromMe',
|
||||
filter['filter.fromMe'],
|
||||
);
|
||||
query = query.whereRaw(sql, [value]);
|
||||
}
|
||||
if (filter['filter.ack'] != null) {
|
||||
const status = AckToStatus(filter['filter.ack']);
|
||||
const [sql, value] = this.repository.filterJson('data', 'status', status);
|
||||
query = query.whereRaw(sql, [value]);
|
||||
}
|
||||
query = this.repository.pagination(query, pagination);
|
||||
return this.repository.all(query);
|
||||
}
|
||||
|
||||
async getByJidById(jid: string, id: string): Promise<any> {
|
||||
if (jid === ALL_JID) {
|
||||
return this.repository.getBy({ id: id });
|
||||
}
|
||||
return this.repository.getBy({ jid: jid, id: id });
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Key } from 'node-cache';
|
||||
import { Page } from 'puppeteer';
|
||||
|
||||
export interface CallErrorEvent {
|
||||
method: string | symbol;
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
export const PAGE_CALL_ERROR_EVENT = 'page.call.error';
|
||||
|
||||
/**
|
||||
* WPage is a puppeter.Page wrapper that use ALL original methods from Page,
|
||||
* but in case if there were any errors in the call (original) -
|
||||
* it calls the errorCallback with method name, error and arguments
|
||||
*
|
||||
* Find in webjs used functions like "pupPage.{function}"
|
||||
* >>> cd src
|
||||
* >>> grep -rho 'page\.[a-zA-Z0-9_]\+(' . --include="*.js" | sed 's/($//' | sort -u
|
||||
*
|
||||
* Find in webjs used functions like "page.{function}"
|
||||
* >>> cd src
|
||||
* >>> grep -rho 'page\.[a-zA-Z0-9_]\+(' . --include="*.js" | sed 's/($//' | sort -u
|
||||
*
|
||||
*/
|
||||
export class WPage extends EventEmitter {
|
||||
constructor(private page: Page) {
|
||||
super();
|
||||
}
|
||||
|
||||
async evaluate(fn, ...args: any[]) {
|
||||
try {
|
||||
return await this.page.evaluate(fn, ...args);
|
||||
} catch (err) {
|
||||
this.emit(PAGE_CALL_ERROR_EVENT, { method: 'evaluate', error: err });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async screenshot(options) {
|
||||
try {
|
||||
return await this.page.screenshot(options);
|
||||
} catch (err) {
|
||||
this.emit(PAGE_CALL_ERROR_EVENT, { method: 'screenshot', error: err });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// waitForNavigation
|
||||
async waitForNavigation(options: any) {
|
||||
try {
|
||||
return await this.page.waitForNavigation(options);
|
||||
} catch (err) {
|
||||
this.emit(PAGE_CALL_ERROR_EVENT, {
|
||||
method: 'waitForNavigation',
|
||||
error: err,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// waitForFunction
|
||||
async waitForFunction(fn, options, ...args) {
|
||||
try {
|
||||
return await this.page.waitForFunction(fn, options, ...args);
|
||||
} catch (err) {
|
||||
this.emit(PAGE_CALL_ERROR_EVENT, {
|
||||
method: 'waitForFunction',
|
||||
error: err,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// setRequestInterception
|
||||
async setRequestInterception(value: boolean) {
|
||||
try {
|
||||
return await this.page.setRequestInterception(value);
|
||||
} catch (err) {
|
||||
this.emit(PAGE_CALL_ERROR_EVENT, {
|
||||
method: 'setRequestInterception',
|
||||
error: err,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// exposeFunction
|
||||
async exposeFunction(name: string, fn: (...args: any[]) => any) {
|
||||
try {
|
||||
return await this.page.exposeFunction(name, fn);
|
||||
} catch (err) {
|
||||
this.emit(PAGE_CALL_ERROR_EVENT, {
|
||||
method: 'exposeFunction',
|
||||
error: err,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
return this;
|
||||
}
|
||||
this.page.on(event, listener);
|
||||
return this.page as any;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
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';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { LoadWAHA } = require('./_WAHA.js');
|
||||
@@ -22,6 +28,7 @@ const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory');
|
||||
|
||||
export class WebjsClientCore extends Client {
|
||||
public events = new EventEmitter();
|
||||
private wpage: WPage = null;
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
@@ -32,12 +39,44 @@ export class WebjsClientCore extends Client {
|
||||
});
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
const result = await super.initialize();
|
||||
if (this.pupPage && !(this.pupPage instanceof WPage)) {
|
||||
this.wpage = new WPage(this.pupPage);
|
||||
this.wpage.on(PAGE_CALL_ERROR_EVENT as any, (event: CallErrorEvent) => {
|
||||
this.events.emit(PAGE_CALL_ERROR_EVENT as any, event);
|
||||
});
|
||||
this.pupPage = this.wpage as any as Page;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async injectWaha() {
|
||||
await this.pupPage.evaluate(LoadLodash);
|
||||
await this.pupPage.evaluate(LoadPaginator);
|
||||
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,
|
||||
@@ -51,6 +90,7 @@ export class WebjsClientCore extends Client {
|
||||
|
||||
async destroy() {
|
||||
this.events.removeAllListeners();
|
||||
this.wpage?.removeAllListeners();
|
||||
await super.destroy();
|
||||
}
|
||||
|
||||
@@ -82,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(
|
||||
@@ -92,7 +132,7 @@ export class WebjsClientCore extends Client {
|
||||
},
|
||||
name,
|
||||
color,
|
||||
);
|
||||
)) as any;
|
||||
return labelId;
|
||||
}
|
||||
|
||||
@@ -119,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();
|
||||
}
|
||||
@@ -128,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));
|
||||
}
|
||||
@@ -199,6 +243,9 @@ export class WebjsClientCore extends Client {
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (filter['filter.ack'] != null && m.ack !== filter['filter.ack']) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -210,8 +257,17 @@ export class WebjsClientCore extends Client {
|
||||
const loadedMessages =
|
||||
// @ts-ignore
|
||||
await window.Store.ConversationMsgs.loadEarlierMsgs(chat);
|
||||
if (!loadedMessages || !loadedMessages.length) break;
|
||||
if (!loadedMessages || loadedMessages.length == 0) break;
|
||||
|
||||
msgs = [...loadedMessages.filter(msgFilter), ...msgs];
|
||||
msgs = msgs.sort((a, b) => b.t - a.t);
|
||||
|
||||
// Check if the earliest message is already outside the timerange filter
|
||||
const earliest = msgs[msgs.length - 1];
|
||||
if (earliest.t < (filter['filter.timestamp.gte'] || Infinity)) {
|
||||
// Add only messages that pass the filter and stop loading more
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (msgs.length > pagination.limit + pagination.offset) {
|
||||
@@ -233,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);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ exports.LoadPaginator = () => {
|
||||
}
|
||||
return window.lodash.orderBy(
|
||||
data,
|
||||
[this.pagination.sortBy],
|
||||
[this.NullLast(this.pagination.sortBy)],
|
||||
[this.pagination.sortOrder || 'asc'],
|
||||
);
|
||||
}
|
||||
@@ -30,6 +30,16 @@ exports.LoadPaginator = () => {
|
||||
const limit = this.pagination.limit || Infinity;
|
||||
return data.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
NullLast(field) {
|
||||
return (item) => {
|
||||
const value = item?.[field];
|
||||
if (value == null) {
|
||||
return -Infinity;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
}
|
||||
window.Paginator = Paginator;
|
||||
};
|
||||
@@ -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));
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
areJidsSameUser,
|
||||
BinaryNode,
|
||||
getBinaryNodeChildren,
|
||||
getStatusFromReceiptType,
|
||||
isJidGroup,
|
||||
isJidStatusBroadcast,
|
||||
jidEncode,
|
||||
jidNormalizedUser,
|
||||
proto,
|
||||
} from '@adiwajshing/baileys';
|
||||
|
||||
export interface ReceiptEvent {
|
||||
key: proto.IMessageKey;
|
||||
participant?: string;
|
||||
messageIds: string[];
|
||||
status: number;
|
||||
_node: any;
|
||||
}
|
||||
|
||||
interface Me {
|
||||
id: string;
|
||||
lid?: string;
|
||||
}
|
||||
|
||||
export function jid(field: any) {
|
||||
if (!field) {
|
||||
return field;
|
||||
}
|
||||
const data = field['$1'];
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
let server = data.server;
|
||||
if (!server) {
|
||||
server =
|
||||
data.domainType === 0 || data.domainType === 128
|
||||
? 's.whatsapp.net'
|
||||
: 'lid';
|
||||
}
|
||||
return jidEncode(data.user, server, data.device);
|
||||
}
|
||||
|
||||
export function TagReceiptNodeToReceiptEvent(
|
||||
node: BinaryNode,
|
||||
me: Me,
|
||||
): ReceiptEvent[] {
|
||||
const { attrs, content } = node;
|
||||
const status = getStatusFromReceiptType(attrs.type);
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const from = jidNormalizedUser(jid(attrs.from));
|
||||
const participant = jidNormalizedUser(jid(attrs.participant));
|
||||
const recipient = jidNormalizedUser(jid(attrs.recipient));
|
||||
|
||||
const isLid = from.includes('lid');
|
||||
const isNodeFromMe = areJidsSameUser(
|
||||
participant || from,
|
||||
isLid ? me?.lid : me?.id,
|
||||
);
|
||||
const remoteJid = !isNodeFromMe || isJidGroup(from) ? from : recipient;
|
||||
const fromMe = !recipient || (attrs.type === 'retry' && isNodeFromMe);
|
||||
|
||||
// basically, we only want to know when a message from us has been delivered to/read by the other person
|
||||
// or another device of ours has read some messages
|
||||
if (status < proto.WebMessageInfo.Status.SERVER_ACK && isNodeFromMe) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const key: proto.IMessageKey = {
|
||||
remoteJid: remoteJid,
|
||||
id: '',
|
||||
fromMe: fromMe,
|
||||
};
|
||||
|
||||
const ids = [attrs.id];
|
||||
if (Array.isArray(content)) {
|
||||
const items = getBinaryNodeChildren(content[0], 'item');
|
||||
ids.push(...items.map((i) => i.attrs.id));
|
||||
}
|
||||
|
||||
if (isJidGroup(remoteJid) || isJidStatusBroadcast(remoteJid)) {
|
||||
if (participant) {
|
||||
key.participant = fromMe ? (isLid ? me.lid : me.id) : recipient;
|
||||
const eventParticipant = fromMe ? participant : isLid ? me.lid : me.id;
|
||||
return [
|
||||
{
|
||||
key: key,
|
||||
messageIds: ids,
|
||||
status: status as any,
|
||||
participant: eventParticipant,
|
||||
_node: node,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
// Handle grouped receipts
|
||||
return handleGroupedReceipts(node, key, status, fromMe, isLid, me);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
key: key,
|
||||
messageIds: ids,
|
||||
status: status as any,
|
||||
_node: node,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function handleGroupedReceipts(
|
||||
node: BinaryNode,
|
||||
key: proto.IMessageKey,
|
||||
status: number,
|
||||
fromMe: boolean,
|
||||
isLid: boolean,
|
||||
me: Me,
|
||||
): ReceiptEvent[] | null {
|
||||
const { content } = node;
|
||||
if (!Array.isArray(content)) {
|
||||
return [];
|
||||
}
|
||||
const participantsTags = content.filter((c) => c.tag === 'participants');
|
||||
if (participantsTags.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const receiptEvents: ReceiptEvent[] = [];
|
||||
|
||||
for (const participants of participantsTags) {
|
||||
const participantKey = participants.attrs?.key;
|
||||
if (!participantKey) continue;
|
||||
|
||||
const users = getBinaryNodeChildren(participants, 'user');
|
||||
for (const user of users) {
|
||||
const userAttrs = user.attrs;
|
||||
if (!userAttrs) continue;
|
||||
|
||||
const userJid = jidNormalizedUser(jid(userAttrs.jid));
|
||||
if (!userJid) continue;
|
||||
|
||||
key.participant = fromMe ? (isLid ? me.lid : me.id) : userJid;
|
||||
const eventParticipant = fromMe ? userJid : isLid ? me.lid : me.id;
|
||||
const receiptEvent: ReceiptEvent = {
|
||||
key: {
|
||||
...key,
|
||||
id: participantKey,
|
||||
},
|
||||
messageIds: [participantKey],
|
||||
status: status as any,
|
||||
participant: eventParticipant,
|
||||
_node: node,
|
||||
};
|
||||
|
||||
receiptEvents.push(receiptEvent);
|
||||
}
|
||||
}
|
||||
|
||||
return receiptEvents;
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,17 @@
|
||||
import { isJidGroup, isJidStatusBroadcast } from '@adiwajshing/baileys';
|
||||
import { UnprocessableEntityException } from '@nestjs/common';
|
||||
import {
|
||||
getChannelInviteLink,
|
||||
WhatsappSession,
|
||||
} from '@waha/core/abc/session.abc';
|
||||
import {
|
||||
getFromToParticipant,
|
||||
toCusFormat,
|
||||
} from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import {
|
||||
ReceiptEvent,
|
||||
TagReceiptNodeToReceiptEvent,
|
||||
} from '@waha/core/engines/webjs/ack.webjs';
|
||||
import {
|
||||
ToGroupV2JoinEvent,
|
||||
ToGroupV2LeaveEvent,
|
||||
@@ -10,13 +19,27 @@ 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,
|
||||
PAGE_CALL_ERROR_EVENT,
|
||||
} from '@waha/core/engines/webjs/WPage';
|
||||
import {
|
||||
AvailableInPlusVersion,
|
||||
NotImplementedByEngineError,
|
||||
} from '@waha/core/exceptions';
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { QR } from '@waha/core/QR';
|
||||
import { StatusToAck } from '@waha/core/utils/acks';
|
||||
import {
|
||||
parseMessageIdSerialized,
|
||||
SerializeMessageKey,
|
||||
} from '@waha/core/utils/ids';
|
||||
import { DistinctAck } from '@waha/core/utils/reactive';
|
||||
import { splitAt } from '@waha/helpers';
|
||||
import { PairingCodeResponse } from '@waha/structures/auth.dto';
|
||||
import {
|
||||
@@ -36,6 +59,9 @@ import {
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
OverviewFilter,
|
||||
ReadChatMessagesQuery,
|
||||
ReadChatMessagesResponse,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import {
|
||||
ChatRequest,
|
||||
@@ -72,26 +98,30 @@ 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 {
|
||||
EnginePayload,
|
||||
WAMessageAckBody,
|
||||
WAMessageRevokedBody,
|
||||
} from '@waha/structures/webhooks.dto';
|
||||
import { PaginatorInMemory } from '@waha/utils/Paginator';
|
||||
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner';
|
||||
import * as lodash from 'lodash';
|
||||
import { filter, fromEvent, merge, mergeMap, Observable } from 'rxjs';
|
||||
import { ProtocolError } from 'puppeteer';
|
||||
import { filter, fromEvent, merge, mergeMap, Observable, share } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import {
|
||||
AuthStrategy,
|
||||
Call,
|
||||
Channel as WEBJSChannel,
|
||||
Chat,
|
||||
@@ -109,6 +139,8 @@ import {
|
||||
} 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';
|
||||
@@ -235,15 +267,36 @@ 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();
|
||||
});
|
||||
|
||||
// Listen for page close event
|
||||
this.whatsapp.pupPage.on('close', () => {
|
||||
this.logger.error('The WhatsApp Web page has been closed');
|
||||
this.failed();
|
||||
});
|
||||
|
||||
// Listen for function call errors
|
||||
this.whatsapp.events.on(
|
||||
PAGE_CALL_ERROR_EVENT,
|
||||
(event: CallErrorEvent) => {
|
||||
if (event.error instanceof ProtocolError) {
|
||||
this.logger.error(
|
||||
`ProtocolError when calling page method: ${String(
|
||||
event.method,
|
||||
)}, restarting client...`,
|
||||
);
|
||||
this.logger.error(event.error);
|
||||
this.failed();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Listen for page error event
|
||||
if (this.isDebugEnabled()) {
|
||||
this.logger.debug("Logging 'console' event for web page");
|
||||
@@ -302,10 +355,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
|
||||
private async end() {
|
||||
this.engineStateCheckDelayedJob.cancel();
|
||||
this.whatsapp?.removeAllListeners();
|
||||
this.whatsapp?.pupBrowser?.removeAllListeners();
|
||||
this.whatsapp?.pupPage?.removeAllListeners();
|
||||
|
||||
try {
|
||||
this.whatsapp?.removeAllListeners();
|
||||
this.whatsapp?.pupBrowser?.removeAllListeners();
|
||||
this.whatsapp?.pupPage?.removeAllListeners();
|
||||
// It's possible that browser yet starting
|
||||
await waitUntil(
|
||||
async () => {
|
||||
@@ -316,11 +370,30 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
1_000,
|
||||
10_000,
|
||||
);
|
||||
this.whatsapp?.destroy().catch((error) => {
|
||||
this.logger.warn(error, 'Failed to destroy the client');
|
||||
});
|
||||
this.logger.debug(
|
||||
'Successfully waited for browser to be ready for closing',
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
this.logger.error(
|
||||
error,
|
||||
'Failed while waiting for browser to be ready for closing',
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.whatsapp?.destroy();
|
||||
this.logger.debug('Successfully destroyed whatsapp client');
|
||||
} catch (error) {
|
||||
this.logger.error(error, 'Failed to destroy whatsapp client');
|
||||
}
|
||||
|
||||
try {
|
||||
// @ts-ignore
|
||||
const strategy: AuthStrategy = this.whatsapp?.authStrategy;
|
||||
await strategy?.destroy();
|
||||
this.logger.debug('Successfully destroyed auth strategy');
|
||||
} catch (error) {
|
||||
this.logger.error(error, 'Failed to destroy auth strategy');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,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!`);
|
||||
@@ -615,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';
|
||||
@@ -624,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) {
|
||||
@@ -691,6 +781,17 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
return result;
|
||||
}
|
||||
|
||||
public async readChatMessages(
|
||||
chatId: string,
|
||||
request: ReadChatMessagesQuery,
|
||||
): Promise<ReadChatMessagesResponse> {
|
||||
const chat: Chat = await this.whatsapp.getChatById(
|
||||
this.ensureSuffix(chatId),
|
||||
);
|
||||
await chat.sendSeen();
|
||||
return { ids: null };
|
||||
}
|
||||
|
||||
public async getChatMessage(
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
@@ -698,6 +799,19 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
): Promise<null | WAMessage> {
|
||||
const message = await this.whatsapp.getMessageById(messageId);
|
||||
if (!message) return null;
|
||||
if (
|
||||
isJidGroup(message.id.remote) ||
|
||||
isJidStatusBroadcast(message.id.remote)
|
||||
) {
|
||||
// @ts-ignore
|
||||
message.rawData.receipts = await message.getInfo().catch((error) => {
|
||||
this.logger.error(
|
||||
{ error: error, msg: message.id._serialized },
|
||||
'Failed to get receipts',
|
||||
);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return await this.processIncomingMessage(message, query.downloadMedia);
|
||||
}
|
||||
|
||||
@@ -840,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
|
||||
*/
|
||||
@@ -1143,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
|
||||
*/
|
||||
@@ -1240,18 +1432,31 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_REACTION).switch(messagesReaction$);
|
||||
|
||||
const messageAck$ = fromEvent(
|
||||
const messageAckWEBJS$ = fromEvent(
|
||||
this.whatsapp,
|
||||
Events.MESSAGE_ACK,
|
||||
(message, ack) => {
|
||||
return { message, ack };
|
||||
},
|
||||
);
|
||||
const messagesAck$ = messageAck$.pipe(
|
||||
const messagesAckDM$ = messageAckWEBJS$.pipe(
|
||||
map((event) => event.message),
|
||||
map(this.toWAMessage.bind(this)),
|
||||
map<any, WAMessage>(this.toWAMessage.bind(this)),
|
||||
filter((ack) => !isJidGroup(ack.to) && !isJidStatusBroadcast(ack.to)),
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_ACK).switch(messagesAck$);
|
||||
const tagReceiptNode$ = fromEvent(this.whatsapp, Events.TAG_RECEIPT);
|
||||
const messageAckGroups$ = tagReceiptNode$.pipe(
|
||||
mergeMap((node) =>
|
||||
TagReceiptNodeToReceiptEvent(node as any, this.getSessionMeInfo()),
|
||||
),
|
||||
mergeMap(this.TagReceiptToMessageAck.bind(this)),
|
||||
filter((ack) => isJidGroup(ack.to) || isJidStatusBroadcast(ack.to)),
|
||||
);
|
||||
|
||||
const messageAckAll$ = merge(messagesAckDM$, messageAckGroups$);
|
||||
|
||||
const messageAck$ = messageAckAll$.pipe(DistinctAck());
|
||||
this.events2.get(WAHAEvents.MESSAGE_ACK).switch(messageAck$);
|
||||
|
||||
//
|
||||
// Others
|
||||
@@ -1259,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
|
||||
//
|
||||
@@ -1383,15 +1604,44 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
};
|
||||
}
|
||||
|
||||
protected TagReceiptToMessageAck(receipt: ReceiptEvent): WAMessageAckBody[] {
|
||||
const ids = receipt.messageIds;
|
||||
const acks = [];
|
||||
for (const id_ of ids) {
|
||||
const messageKey = {
|
||||
fromMe: receipt.key.fromMe,
|
||||
remoteJid: toCusFormat(receipt.key.remoteJid),
|
||||
participant: toCusFormat(receipt.key.participant),
|
||||
id: id_,
|
||||
};
|
||||
const fromToParticipant = getFromToParticipant(messageKey);
|
||||
const id = SerializeMessageKey(messageKey);
|
||||
const ack = StatusToAck(receipt.status);
|
||||
acks.push({
|
||||
id: id,
|
||||
from: fromToParticipant.from,
|
||||
to: fromToParticipant.to,
|
||||
participant: toCusFormat(receipt.participant),
|
||||
fromMe: !receipt.key.fromMe, // reverted, it's right
|
||||
ack: ack,
|
||||
ackName: WAMessageAck[ack] || ACK_UNKNOWN,
|
||||
_data: receipt._node,
|
||||
});
|
||||
}
|
||||
return acks;
|
||||
}
|
||||
|
||||
protected toWAMessage(message: Message): WAMessage {
|
||||
const replyTo = this.extractReplyTo(message);
|
||||
const source = this.getMessageSource(message.id.id);
|
||||
const key = parseMessageIdSerialized(message.id._serialized);
|
||||
// @ts-ignore
|
||||
return {
|
||||
id: message.id._serialized,
|
||||
timestamp: message.timestamp,
|
||||
from: message.from,
|
||||
fromMe: message.fromMe,
|
||||
participant: toCusFormat(key.participant),
|
||||
source: source,
|
||||
to: message.to,
|
||||
body: message.body,
|
||||
@@ -1427,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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
@@ -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;');
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface IJsonQuery {
|
||||
filter(field: string, key: string): string;
|
||||
filter(field: string, key: string, value: any): [string, string];
|
||||
sortBy(field: string, sortBy: string, direction: string): string;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { ISQLEngine } from '@waha/core/storage/sql/ISQLEngine';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { KnexPaginator } from '@waha/utils/Paginator';
|
||||
import Knex from 'knex';
|
||||
import * as lodash from 'lodash';
|
||||
|
||||
export type Migration = string;
|
||||
|
||||
@@ -79,17 +80,26 @@ export class SqlKVRepository<Entity> {
|
||||
}
|
||||
|
||||
private async upsertBatch(entities: Entity[]): Promise<void> {
|
||||
const data = entities.map((entity) => this.dump(entity));
|
||||
const all = entities.map((entity) => this.dump(entity));
|
||||
// make it unique by .id
|
||||
const data = lodash.uniqBy(all, (d: any) => d.id);
|
||||
if (data.length != all.length) {
|
||||
console.warn(
|
||||
`WARNING - Duplicated entities for upsert batch: ${JSON.stringify(
|
||||
entities,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
const columns = this.columns.map((c) => `"${c.fieldName}"`);
|
||||
const values = data.map((d) => Object.values(d)).flat();
|
||||
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) {
|
||||
@@ -109,8 +119,43 @@ export class SqlKVRepository<Entity> {
|
||||
return this.all(query);
|
||||
}
|
||||
|
||||
getAllByIds(ids: string[]) {
|
||||
return this.all(this.select().whereIn('id', ids));
|
||||
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(
|
||||
(entity) => entity !== null,
|
||||
) as Entity[];
|
||||
}
|
||||
|
||||
async getEntitiesByIds(ids: string[]): Promise<Map<string, Entity | null>> {
|
||||
if (ids.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const rows = await this.engine.all(this.select().whereIn('id', ids));
|
||||
const entitiesMap = new Map<string, Entity | null>();
|
||||
|
||||
// Initialize a map with null values for all requested IDs
|
||||
for (const id of ids) {
|
||||
entitiesMap.set(id, null);
|
||||
}
|
||||
|
||||
// Fill in the map with found entities
|
||||
for (const row of rows) {
|
||||
if (row && row.id) {
|
||||
entitiesMap.set(row.id, this.parse(row));
|
||||
}
|
||||
}
|
||||
|
||||
return entitiesMap;
|
||||
}
|
||||
|
||||
getById(id: string): Promise<Entity | null> {
|
||||
@@ -187,8 +232,8 @@ export class SqlKVRepository<Entity> {
|
||||
/**
|
||||
* JSON helpers
|
||||
*/
|
||||
public filterJson(field: string, key: string): string {
|
||||
return this.jsonQuery.filter(field, key);
|
||||
public filterJson(field: string, key: string, value: any): [string, string] {
|
||||
return this.jsonQuery.filter(field, key, value);
|
||||
}
|
||||
|
||||
protected stringify(data: any): string {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { IJsonQuery } from '@waha/core/storage/sql/IJsonQuery';
|
||||
|
||||
export class Sqlite3JsonQuery implements IJsonQuery {
|
||||
filter(field: string, key: string): string {
|
||||
filter(field: string, key: string, value: any): [string, string] {
|
||||
const jsonPath = `$.${key}`;
|
||||
return `${field}->'${jsonPath}' = ?`;
|
||||
return [`${field}->'${jsonPath}' = ?`, `${value}`];
|
||||
}
|
||||
|
||||
sortBy(field: string, sortBy: string, direction: string): string {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { WAMessageAck } from '@waha/structures/enums.dto';
|
||||
|
||||
export function StatusToAck(status: number): WAMessageAck {
|
||||
return status - 1;
|
||||
}
|
||||
|
||||
export function AckToStatus(ack: number): number {
|
||||
return ack + 1;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { isJidGroup, WAMessageKey } from '@adiwajshing/baileys';
|
||||
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
|
||||
import { toJID } from '@waha/core/utils/jids';
|
||||
import {
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
ReadChatMessagesQuery,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import { SendSeenRequest } from '@waha/structures/chatting.dto';
|
||||
import { WAMessageAck } from '@waha/structures/enums.dto';
|
||||
|
||||
export function ExtractMessageKeysForRead(
|
||||
request: SendSeenRequest,
|
||||
): WAMessageKey[] {
|
||||
const jid = toJID(request.chatId);
|
||||
const defaults = {
|
||||
remoteJid: jid,
|
||||
participant: request.participant,
|
||||
};
|
||||
const ids = request.messageIds || [];
|
||||
if (request.messageId) {
|
||||
ids.push(request.messageId);
|
||||
}
|
||||
const keys: WAMessageKey[] = [];
|
||||
for (const messageId of ids) {
|
||||
const parsed = parseMessageIdSerialized(messageId, false);
|
||||
if (parsed.fromMe) {
|
||||
continue;
|
||||
}
|
||||
const key: WAMessageKey = {
|
||||
id: parsed.id,
|
||||
remoteJid: parsed.remoteJid
|
||||
? toJID(parsed.remoteJid)
|
||||
: defaults.remoteJid,
|
||||
participant: parsed.participant
|
||||
? toJID(parsed.participant)
|
||||
: defaults.participant,
|
||||
};
|
||||
keys.push(key);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function daysToMs(days: number) {
|
||||
return days * 24 * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
export function MessagesForRead(
|
||||
chatId: string,
|
||||
request: ReadChatMessagesQuery,
|
||||
): {
|
||||
query: GetChatMessagesQuery;
|
||||
filter: GetChatMessagesFilter;
|
||||
} {
|
||||
const limit = request.messages || isJidGroup(chatId) ? 100 : 30;
|
||||
const query: GetChatMessagesQuery = {
|
||||
offset: 0,
|
||||
limit: limit,
|
||||
downloadMedia: false,
|
||||
};
|
||||
const afterMs = Date.now() - daysToMs(request.days);
|
||||
const after = Math.floor(afterMs / 1000);
|
||||
const filter: GetChatMessagesFilter = {
|
||||
'filter.ack': WAMessageAck.DEVICE,
|
||||
'filter.fromMe': false,
|
||||
'filter.timestamp.gte': after,
|
||||
};
|
||||
return {
|
||||
query: query,
|
||||
filter: filter,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { WAMessageKey } from '@adiwajshing/baileys';
|
||||
import { toJID } from '@waha/core/utils/jids';
|
||||
|
||||
/**
|
||||
* Parse message id from WAHA to engine
|
||||
* false_11111111111@c.us_AAA
|
||||
* {id: "AAA", remoteJid: "11111111111@s.whatsapp.net", "fromMe": false}
|
||||
*/
|
||||
export function parseMessageIdSerialized(
|
||||
messageId: string,
|
||||
soft: boolean = false,
|
||||
): WAMessageKey {
|
||||
if (!messageId.includes('_') && soft) {
|
||||
return { id: messageId };
|
||||
}
|
||||
|
||||
const parts = messageId.split('_');
|
||||
if (parts.length != 3 && parts.length != 4) {
|
||||
throw new Error(
|
||||
'Message id be in format false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA[_participant]',
|
||||
);
|
||||
}
|
||||
const fromMe = parts[0] == 'true';
|
||||
const chatId = parts[1];
|
||||
const remoteJid = toJID(chatId);
|
||||
const id = parts[2];
|
||||
const participant = parts[3] ? toJID(parts[3]) : undefined;
|
||||
return {
|
||||
fromMe: fromMe,
|
||||
id: id,
|
||||
remoteJid: remoteJid,
|
||||
participant: participant,
|
||||
};
|
||||
}
|
||||
|
||||
export function SerializeMessageKey(key: WAMessageKey) {
|
||||
const { fromMe, id, remoteJid, participant } = key;
|
||||
const participantStr = participant ? `_${participant}` : '';
|
||||
return `${fromMe ? 'true' : 'false'}_${remoteJid}_${id}${participantStr}`;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { isJidGroup, isJidMetaIa } from '@adiwajshing/baileys';
|
||||
import {
|
||||
isJidBroadcast,
|
||||
isLidUser,
|
||||
} from '@adiwajshing/baileys/lib/WABinary/jid-utils';
|
||||
|
||||
export function isJidNewsletter(jid: string) {
|
||||
return jid.endsWith('@newsletter');
|
||||
}
|
||||
|
||||
export function isJidCus(jid: string) {
|
||||
return jid?.endsWith('@c.us');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from 11111111111@c.us to 11111111111@s.whatsapp.net
|
||||
* @param chatId
|
||||
*/
|
||||
export function toJID(chatId) {
|
||||
if (isJidGroup(chatId)) {
|
||||
return chatId;
|
||||
}
|
||||
if (isJidBroadcast(chatId)) {
|
||||
return chatId;
|
||||
}
|
||||
if (isJidNewsletter(chatId)) {
|
||||
return chatId;
|
||||
}
|
||||
if (isLidUser(chatId)) {
|
||||
return chatId;
|
||||
}
|
||||
if (isJidMetaIa(chatId)) {
|
||||
return chatId;
|
||||
}
|
||||
const number = chatId.split('@')[0];
|
||||
return number + '@s.whatsapp.net';
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { WAMessageAckBody } from '@waha/structures/webhooks.dto';
|
||||
import { distinct, interval } from 'rxjs';
|
||||
|
||||
export function DistinctAck(flushEvery: number = 60_000) {
|
||||
// only if we haven’t seen this key since the last flush
|
||||
return distinct(
|
||||
(msg: WAMessageAckBody) => `${msg.id}-${msg.ack}-${msg.participant}`,
|
||||
interval(flushEvery),
|
||||
);
|
||||
}
|
||||
@@ -195,5 +195,5 @@ export class PreviewChannelMessages {
|
||||
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
limit: number = 100;
|
||||
limit: number = 10;
|
||||
}
|
||||
@@ -1,12 +1,23 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { BooleanString } from '@waha/nestjs/validation/BooleanString';
|
||||
import { WAMessageAck, WAMessageAckName } from '@waha/structures/enums.dto';
|
||||
import {
|
||||
LimitOffsetParams,
|
||||
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
|
||||
@@ -39,6 +50,30 @@ export class GetChatMessagesFilter {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
'filter.fromMe'?: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: 'Filter messages by acknowledgment status',
|
||||
enum: WAMessageAckName,
|
||||
})
|
||||
@IsEnum(WAMessageAckName)
|
||||
@IsOptional()
|
||||
'filter.ack'?: WAMessageAck;
|
||||
}
|
||||
|
||||
export function transformAck(
|
||||
filter: GetChatMessagesFilter,
|
||||
): GetChatMessagesFilter {
|
||||
if (!filter) return filter;
|
||||
if (!filter['filter.ack']) return filter;
|
||||
const ackName = filter['filter.ack'];
|
||||
// @ts-ignore
|
||||
const ack: WAMessageAck = WAMessageAck[ackName];
|
||||
if (ack == null) {
|
||||
throw new BadRequestException(`Invalid ack: '${ackName}'`);
|
||||
}
|
||||
filter['filter.ack'] = ack;
|
||||
return filter;
|
||||
}
|
||||
|
||||
export class ChatPictureQuery {
|
||||
@@ -62,7 +97,7 @@ export class GetChatMessagesQuery {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
limit: number = 100;
|
||||
limit: number = 10;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@@ -80,6 +115,35 @@ export class GetChatMessagesQuery {
|
||||
downloadMedia: boolean = true;
|
||||
}
|
||||
|
||||
export class ReadChatMessagesQuery {
|
||||
@ApiProperty({
|
||||
example: 30,
|
||||
required: false,
|
||||
description: 'How much messages to read (latest first)',
|
||||
})
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
messages: number;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: 'How much days to read (latest first)',
|
||||
})
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
days: number = 7;
|
||||
}
|
||||
|
||||
export class ReadChatMessagesResponse {
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: 'Messages IDs that have been read',
|
||||
})
|
||||
ids?: string[];
|
||||
}
|
||||
|
||||
export class GetChatMessageQuery {
|
||||
@ApiProperty({
|
||||
example: true,
|
||||
@@ -132,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;
|
||||
|
||||
@@ -82,19 +82,24 @@ export class ChatRequest extends SessionBaseRequest {
|
||||
|
||||
export class SendSeenRequest extends ChatRequest {
|
||||
@ApiProperty({
|
||||
example: 'false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
|
||||
example: null,
|
||||
deprecated: true,
|
||||
required: false,
|
||||
description:
|
||||
"NOWEB engine only - it's important to mark ALL messages as seen",
|
||||
})
|
||||
messageId?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: ['false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA'],
|
||||
required: false,
|
||||
})
|
||||
messageIds?: string[];
|
||||
|
||||
@ApiProperty({
|
||||
example: '11111111111@c.us',
|
||||
required: false,
|
||||
default: null,
|
||||
description:
|
||||
'NOWEB engine only - the ID of the user that sent the message (undefined for individual chats)',
|
||||
'NOWEB engine only - the ID of the user that sent the message (undefined for individual chats)',
|
||||
})
|
||||
participant?: string;
|
||||
}
|
||||
@@ -159,6 +164,9 @@ export class MessageContactVcardRequest extends ChatRequest {
|
||||
],
|
||||
})
|
||||
contacts: (VCardContact | Contact)[];
|
||||
|
||||
@ReplyToProperty()
|
||||
reply_to?: string;
|
||||
}
|
||||
|
||||
export class MessageTextRequest extends ChatRequest {
|
||||
@@ -412,6 +420,7 @@ export class MessageDestination {
|
||||
to: string;
|
||||
from: string;
|
||||
fromMe: boolean;
|
||||
participant?: string;
|
||||
}
|
||||
|
||||
export class MessageButtonReply extends ChatRequest {
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -68,4 +70,13 @@ export enum WAMessageAck {
|
||||
PLAYED = 4,
|
||||
}
|
||||
|
||||
export enum WAMessageAckName {
|
||||
ERROR = 'ERROR',
|
||||
PENDING = 'PENDING',
|
||||
SERVER = 'SERVER',
|
||||
DEVICE = 'DEVICE',
|
||||
READ = 'READ',
|
||||
PLAYED = 'PLAYED',
|
||||
}
|
||||
|
||||
export const ACK_UNKNOWN = 'UNKNOWN';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export enum MessageSource {
|
||||
APP = 'app',
|
||||
}
|
||||
|
||||
class WAMessageBase {
|
||||
export class WAMessageBase {
|
||||
@MessageIdProperty()
|
||||
id: string;
|
||||
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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,
|
||||
|
||||
+12
-2
@@ -24,8 +24,8 @@ export class PaginatorInMemory extends Paginator {
|
||||
}
|
||||
return lodash.orderBy(
|
||||
data,
|
||||
this.pagination.sortBy,
|
||||
this.pagination.sortOrder || 'asc',
|
||||
[this.NullLast(this.pagination.sortBy)],
|
||||
[this.pagination.sortOrder || 'asc'],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,16 @@ export class PaginatorInMemory extends Paginator {
|
||||
const limit = this.pagination.limit || Infinity;
|
||||
return data.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
NullLast(field) {
|
||||
return (item) => {
|
||||
const value = item?.[field];
|
||||
if (value == null) {
|
||||
return -Infinity;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class KnexPaginator extends Paginator {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { WAHASessionStatus } from '../structures/enums.dto';
|
||||
|
||||
const STUCK_IN_STARTING_THRESHOLD = 20;
|
||||
const STUCK_IN_STARTING_THRESHOLD = 60;
|
||||
|
||||
/**
|
||||
* Keeps track of session statuses (with timestamps) for the last `trackPeriodMs` milliseconds.
|
||||
|
||||
@@ -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
@@ -33,7 +33,7 @@ export function getEngineName(): string {
|
||||
}
|
||||
|
||||
export const VERSION: WAHAEnvironment = {
|
||||
version: '2025.4.1',
|
||||
version: '2025.6.1',
|
||||
engine: getEngineName(),
|
||||
tier: getWAHAVersion(),
|
||||
browser:
|
||||
|
||||
+2
-2
@@ -2,11 +2,11 @@
|
||||
"waha": {
|
||||
"gows": {
|
||||
"repo": "devlikeapro/gows",
|
||||
"ref": "v1.0.3"
|
||||
"ref": "v1.0.8"
|
||||
},
|
||||
"dashboard": {
|
||||
"repo": "devlikeapro/dashboard",
|
||||
"ref": "0e7e2e7edc715b0c30fff89c10ee2d2fc3402d2d"
|
||||
"ref": "3fb6694d849ca0b567679dd59311a50b9d727f2f"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user