Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9171addde | ||
|
|
440ac087ad | ||
|
|
9dd5eb4eeb | ||
|
|
26c2c9abc9 | ||
|
|
663b07c07e | ||
|
|
5add6a0e73 | ||
|
|
5c4912feb4 | ||
|
|
75e115f390 | ||
|
|
8e443d23f4 | ||
|
|
c43c9c319b | ||
|
|
e32b128b0f | ||
|
|
5287421cea | ||
|
|
9bc95ef350 | ||
|
|
028de6addb | ||
|
|
a107605937 | ||
|
|
cec5d0147b | ||
|
|
e46b879fc4 | ||
|
|
b6764fbb29 | ||
|
|
e8899cc314 | ||
|
|
2b2054dd6b | ||
|
|
92705b0ae2 | ||
|
|
2ae8a36b7f | ||
|
|
899139dda1 | ||
|
|
b6b571e84b | ||
|
|
9e098fa997 | ||
|
|
13d00f954f | ||
|
|
fee75d0c8a | ||
|
|
abae3f14cd | ||
|
|
b8a98e2c6e | ||
|
|
d990572b5b | ||
|
|
2d37d392bc | ||
|
|
59015fc3bb | ||
|
|
c3be68f988 | ||
|
|
8610784958 | ||
|
|
f01d71c34a | ||
|
|
a06501e4b1 | ||
|
|
7c2c30fa1b | ||
|
|
903dc714f2 | ||
|
|
b1bd2c8805 | ||
|
|
0e4b4f6d8c | ||
|
|
d8d0eaf247 | ||
|
|
90096faef3 | ||
|
|
f794f75394 | ||
|
|
8de63e486c | ||
|
|
4308efca47 | ||
|
|
877fa292d6 | ||
|
|
df6ea6f497 | ||
|
|
5e9be391e1 | ||
|
|
49960b8234 | ||
|
|
d669dbe54b | ||
|
|
2d1859ac0c | ||
|
|
dc864055a3 | ||
|
|
bad9537aba | ||
|
|
303b1adc0a | ||
|
|
c582904d41 | ||
|
|
65eabf6153 | ||
|
|
cc9c9e2253 | ||
|
|
8747ae9564 | ||
|
|
9253e54958 | ||
|
|
fb774f2caa | ||
|
|
ba120024a6 | ||
|
|
1fe0e69914 | ||
|
|
0fe04e411c | ||
|
|
35074f959f | ||
|
|
0d698fbaf1 | ||
|
|
c9e2a52327 | ||
|
|
a58e06109b | ||
|
|
65375e4836 | ||
|
|
edc517d33f | ||
|
|
354c82dc34 | ||
|
|
fe2acd0665 | ||
|
|
19b5b1a90c | ||
|
|
e7281e2118 | ||
|
|
10f334cf15 | ||
|
|
a057fd6458 | ||
|
|
3bafabeb0a | ||
|
|
edefbe17da | ||
|
|
a65041ffcd | ||
|
|
0962c60844 | ||
|
|
3d72eceac0 | ||
|
|
57b5deff5b | ||
|
|
1163dee352 | ||
|
|
f7fe562bba | ||
|
|
3394f98a8a | ||
|
|
a575b35286 | ||
|
|
0f41e79e98 | ||
|
|
24bf22a31f | ||
|
|
05a1b8802e | ||
|
|
9c409d9507 | ||
|
|
4ee7161c36 | ||
|
|
4cd9ce0ff7 | ||
|
|
a2d0dfd8b2 | ||
|
|
d4164fe0ba | ||
|
|
6bbecdd7c0 | ||
|
|
0784c3ae05 | ||
|
|
679757ccfa | ||
|
|
63a4e693e3 | ||
|
|
b21041d9e0 | ||
|
|
f014bb4587 | ||
|
|
9030f7c4ce | ||
|
|
bde6c38bd4 | ||
|
|
0aefb6c6ec | ||
|
|
da650ccdef |
No files matched your search
@@ -2,6 +2,8 @@
|
||||
example.ts
|
||||
.*sessions
|
||||
.secrets
|
||||
.minio
|
||||
.media
|
||||
tokens
|
||||
files
|
||||
venv
|
||||
@@ -11,6 +13,7 @@ src/test.js
|
||||
test.js
|
||||
.wwebjs_cache
|
||||
src/dashboard
|
||||
.eslintcache
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
|
||||
+15
-1
@@ -6,12 +6,21 @@ exclude: |
|
||||
)$
|
||||
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: commit-message-prefix
|
||||
name: Validate Commit Message for src/plus Changes
|
||||
entry: python ./.precommit/validate_commit_message.py
|
||||
language: python
|
||||
stages: [commit-msg]
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: lint
|
||||
stages: [pre-commit]
|
||||
name: lint
|
||||
language: system
|
||||
entry: bash -c '$HOME/.nvm/nvm-exec npm run lint'
|
||||
entry: bash -c '$HOME/.nvm/nvm-exec npm run lint-cache'
|
||||
pass_filenames: false
|
||||
files: |
|
||||
(?x)^(
|
||||
@@ -22,6 +31,7 @@ repos:
|
||||
rev: 'v3.1.0'
|
||||
hooks:
|
||||
- id: prettier
|
||||
stages: [pre-commit]
|
||||
exclude: |
|
||||
(?x)^(
|
||||
docs/.*|
|
||||
@@ -31,11 +41,13 @@ repos:
|
||||
rev: v1.1.2
|
||||
hooks:
|
||||
- id: markdown-toc
|
||||
stages: [pre-commit]
|
||||
name: README.md
|
||||
files: ^README.md$
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: no-plus-in-core
|
||||
stages: [pre-commit]
|
||||
name: No "plus" in core
|
||||
language: pygrep
|
||||
entry: 'plus'
|
||||
@@ -50,11 +62,13 @@ repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: no-console-log
|
||||
stages: [pre-commit]
|
||||
name: No console.log() calls
|
||||
language: pygrep
|
||||
entry: 'console\.log'
|
||||
exclude: |
|
||||
(?x)^(
|
||||
entrypoint.sh|
|
||||
.pre-commit-config.yaml|
|
||||
^src/core/engines/webjs/session.webjs.core.ts|
|
||||
)$
|
||||
@@ -0,0 +1,38 @@
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def get_staged_files():
|
||||
result = subprocess.run(["git", "diff", "--cached", "--name-only"], stdout=subprocess.PIPE, text=True)
|
||||
files = result.stdout.splitlines()
|
||||
return files
|
||||
|
||||
|
||||
def get_commit_message(commit_msg_filepath):
|
||||
with open(commit_msg_filepath, 'r') as f:
|
||||
return f.readline().strip()
|
||||
|
||||
|
||||
def main():
|
||||
commit_msg_filepath = sys.argv[1]
|
||||
commit_message = get_commit_message(commit_msg_filepath)
|
||||
staged_files = get_staged_files()
|
||||
|
||||
has_plus_changes = any(f.startswith('src/plus') for f in staged_files)
|
||||
print(commit_message)
|
||||
starts_with_plus = commit_message.startswith('[PLUS]')
|
||||
|
||||
if has_plus_changes and not starts_with_plus:
|
||||
print("'[PLUS]' not found in commit message, but there's changes are from 'src/plus'.")
|
||||
return 1
|
||||
|
||||
if starts_with_plus and not all(f.startswith('src/plus') for f in staged_files):
|
||||
print("'[PLUS]' found in commit message, but there's changes from other directories. \n"
|
||||
"Changes MUST be only from 'src/plus'.")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+3
-2
@@ -24,7 +24,7 @@ RUN yarn build && find ./dist -name "*.d.ts" -delete
|
||||
FROM node:${NODE_VERSION} as dashboard
|
||||
|
||||
# Download WAHA Dashboard
|
||||
ENV WAHA_DASHBOARD_SHA 63fd6e9e23e90f90e8aa0de04feb3f1909f63021
|
||||
ENV WAHA_DASHBOARD_SHA c1a91f879a35f0ae32d46ddb2e061ac1819a3d4f
|
||||
RUN \
|
||||
wget https://github.com/devlikeapro/dashboard/archive/${WAHA_DASHBOARD_SHA}.zip \
|
||||
&& unzip ${WAHA_DASHBOARD_SHA}.zip -d /tmp/dashboard \
|
||||
@@ -86,6 +86,7 @@ COPY package.json ./
|
||||
COPY --from=build /src/node_modules ./node_modules
|
||||
COPY --from=build /src/dist ./dist
|
||||
COPY --from=dashboard /dashboard ./dist/dashboard
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
# Chokidar options to monitor file changes
|
||||
ENV CHOKIDAR_USEPOLLING=1
|
||||
@@ -96,4 +97,4 @@ ENV WAHA_ZIPPER=ZIPUNZIP
|
||||
|
||||
# Run command, etc
|
||||
EXPOSE 3000
|
||||
CMD yarn start:prod
|
||||
CMD ["/entrypoint.sh"]
|
||||
@@ -32,7 +32,7 @@ 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
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-channels-2
|
||||
|
||||
up-webjs:
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-webpack-exodus-channels
|
||||
|
||||
@@ -141,7 +141,7 @@ later:
|
||||
How to log in - the instruction on WhatsApp site
|
||||
</a>
|
||||
|
||||
When your ready - find `POST /api/session/start`, click on **Try it out**, then **Execute** a bit below.
|
||||
When your ready - find `POST /api/sessions`, click on **Try it out**, then **Execute** a bit below.
|
||||
|
||||
|
||||
The example payload:
|
||||
|
||||
+54
-14
@@ -57,6 +57,7 @@ services:
|
||||
# If you know the right URL, you can use it here.
|
||||
# Use any port in WHATSAPP_API_PORT and domain in WHATSAPP_API_HOSTNAME
|
||||
# https://waha.devlike.pro/docs/how-to/config/#common
|
||||
#- WHATSAPP_API_SCHEMA=http
|
||||
#- WHATSAPP_API_PORT=3000
|
||||
#- WHATSAPP_API_HOSTNAME=localhost
|
||||
|
||||
@@ -147,11 +148,13 @@ services:
|
||||
# https://waha.devlike.pro/docs/how-to/config/#https
|
||||
# Consider using certbot for HTTPS
|
||||
# https://waha.devlike.pro/blog/setting-up-https-for-waha/#lets-encrypt-certbot--waha
|
||||
#- WHATSAPP_API_HOSTNAME=waha.example.pro
|
||||
#- WAHA_HTTPS_ENABLED=true
|
||||
#- WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
|
||||
#- WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem \
|
||||
#- WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
|
||||
#- WHATSAPP_API_SCHEMA=https
|
||||
#- WHATSAPP_API_PORT=3000
|
||||
#- WHATSAPP_API_HOSTNAME=waha.example.pro
|
||||
#
|
||||
|
||||
#
|
||||
@@ -165,15 +168,30 @@ services:
|
||||
# Download only specific media types
|
||||
#- WHATSAPP_FILES_MIMETYPES=image/jpeg,image/png
|
||||
#
|
||||
|
||||
# Media - Local Storage
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---local
|
||||
#
|
||||
# Set to 0 to keep media files forever
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#save-media-files-between-the-container-restarts
|
||||
- WAHA_MEDIA_STORAGE=LOCAL
|
||||
- WHATSAPP_FILES_LIFETIME=0
|
||||
- WHATSAPP_FILES_FOLDER=/app/.media
|
||||
#
|
||||
# Keep media files for 180 seconds (3 minutes)
|
||||
#- WHATSAPP_FILES_LIFETIME=180
|
||||
#
|
||||
|
||||
# Media - S3 Storage
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
|
||||
#- WAHA_MEDIA_STORAGE=S3
|
||||
#- WAHA_S3_REGION=eu-west-2
|
||||
#- WAHA_S3_BUCKET=waha
|
||||
#- WAHA_S3_ACCESS_KEY_ID=minioadmin
|
||||
#- WAHA_S3_SECRET_ACCESS_KEY=minioadmin
|
||||
#- WAHA_S3_ENDPOINT=http://minio:9000 # Not required if you're using AWS S3
|
||||
#- WAHA_S3_FORCE_PATH_STYLE=True # Required for Minio
|
||||
#- WAHA_S3_PROXY_FILES=True # Required for docker-compose setup
|
||||
|
||||
#
|
||||
# Global Webhooks
|
||||
#
|
||||
@@ -188,16 +206,38 @@ services:
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
|
||||
#- WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongodb:27017
|
||||
#
|
||||
# NOTE: Uncomment this block to run WAHA+ with MongoDB
|
||||
# mongodb:
|
||||
# image: mongo
|
||||
# container_name: mongodb
|
||||
# ports:
|
||||
# - '27017:27017/tcp'
|
||||
# volumes:
|
||||
# - mongodb_data:/data/db
|
||||
# restart: always
|
||||
|
||||
# NOTE: Uncomment this block to run WAHA+ with MongoDB
|
||||
#volumes:
|
||||
# mongodb_data: {}
|
||||
# NOTE: Only if you're using MongoDB
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
|
||||
# Comment this block if you're using MongoDB
|
||||
mongodb:
|
||||
image: mongo
|
||||
container_name: mongodb
|
||||
ports:
|
||||
- '27017:27017/tcp'
|
||||
volumes:
|
||||
- mongodb_data:/data/db
|
||||
restart: always
|
||||
|
||||
# NOTE: Only if you're using S3 to save media files
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
|
||||
# Comment this block if you're using AWS S3
|
||||
minio:
|
||||
image: quay.io/minio/minio
|
||||
container_name: minio
|
||||
restart: always
|
||||
ports:
|
||||
- '9000:9000'
|
||||
- '9001:9001'
|
||||
environment:
|
||||
MINIO_REGION: 'eu-west-2'
|
||||
MINIO_ROOT_USER: 'minioadmin'
|
||||
MINIO_ROOT_PASSWORD: 'minioadmin'
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
command: server /data --console-address ":9001"
|
||||
|
||||
volumes:
|
||||
# NOTE: Only if you're using MongoDB
|
||||
mongodb_data: {}
|
||||
minio_data: {}
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Calculate UV_THREADPOOL_SIZE based on number of CPUs
|
||||
cpus=$(node -e "const os = require('os'); console.log(os.cpus().length);")
|
||||
uv_threadpool_size=$(($cpus * 2))
|
||||
|
||||
# Set UV_THREADPOOL_SIZE as an environment variable
|
||||
export UV_THREADPOOL_SIZE="${UV_THREADPOOL_SIZE:-$uv_threadpool_size}"
|
||||
|
||||
# Start your application using yarn
|
||||
yarn start:prod
|
||||
@@ -50,8 +50,8 @@ docker run -it --rm --network=host -e WHATSAPP_HOOK_URL=http://localhost:5000/bo
|
||||
|
||||
2. Open Swagger API in the browser http://localhost:3000/
|
||||
3. Start session and scan QR code in swagger
|
||||
1. Find `POST /api/session/start` request press **Try it out** and
|
||||
**Execute** it with `default` session name
|
||||
1. Find `POST /api/sessions/` request press **Try it out** and **Execute** it
|
||||
with `default` session name
|
||||
2. Find `GET /api/screenshot` and execute it - it must show QR code
|
||||
3. Scan QR code on your mobile WhatsApp application (that installed on your
|
||||
phone)
|
||||
|
||||
+8
-2
@@ -15,6 +15,7 @@
|
||||
"start:prod": "node dist/main",
|
||||
"start:prod-exit": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix --max-warnings=0",
|
||||
"lint-cache": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix --cache --max-warnings=0",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
@@ -22,11 +23,13 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-channels",
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-channels-2",
|
||||
"@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/config": "^0.5.0",
|
||||
"@nestjs/config": "^3.2.3",
|
||||
"@nestjs/core": "^9.0.9",
|
||||
"@nestjs/passport": "^9.0.0",
|
||||
"@nestjs/platform-express": "^9.0.9",
|
||||
@@ -48,7 +51,9 @@
|
||||
"del": "^6.0.0",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"file-type": "16.5.4",
|
||||
"fs-extra": "^11.2.0",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"joi": "^17.13.3",
|
||||
"knex": "^3.1.0",
|
||||
"libphonenumber-js": "^1.10.36",
|
||||
"link-preview-js": "^3.0.4",
|
||||
@@ -61,6 +66,7 @@
|
||||
"pino-http": "^10.2.0",
|
||||
"pino-pretty": "^11.2.1",
|
||||
"pretty-bytes": "5.6.0",
|
||||
"promise-retry": "^2.0.1",
|
||||
"puppeteer": "^21.0.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
|
||||
@@ -8,9 +8,15 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
|
||||
import {
|
||||
CaptchaBody,
|
||||
OTPRequest,
|
||||
@@ -21,12 +27,10 @@ import {
|
||||
} from '../structures/auth.dto';
|
||||
import { WAHASessionStatus } from '../structures/enums.dto';
|
||||
import { Base64File } from '../structures/files.dto';
|
||||
import { BufferResponseInterceptor } from './BufferResponseInterceptor';
|
||||
import { ApiFileAcceptHeader, SessionApiParam, SessionParam } from './helpers';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/auth')
|
||||
@ApiTags('auth')
|
||||
@ApiTags('🔑 Auth')
|
||||
class AuthController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import {
|
||||
Channel,
|
||||
CreateChannelRequest,
|
||||
@@ -18,11 +22,10 @@ import {
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { isNewsletter, WhatsappSession } from '../core/abc/session.abc';
|
||||
import { SessionApiParam, SessionParam } from './helpers';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/channels')
|
||||
@ApiTags('channels')
|
||||
@ApiTags('📢 Channels')
|
||||
export class ChannelsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -11,22 +11,22 @@ import {
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ChatIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
|
||||
import { MessageIdApiParam } from '@waha/nestjs/params/MessageIdApiParam';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { parseBool } from '../helpers';
|
||||
import { GetChatMessagesQuery, GetChatsQuery } from '../structures/chats.dto';
|
||||
import { EditMessageRequest } from '../structures/chatting.dto';
|
||||
import {
|
||||
ChatIdApiParam,
|
||||
MessageIdApiParam,
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from './helpers';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/chats')
|
||||
@ApiTags('chats')
|
||||
@ApiTags('💬 Chats')
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
class ChatsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@@ -26,7 +26,7 @@ import { WAMessage } from '../structures/responses.dto';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api')
|
||||
@ApiTags('chatting')
|
||||
@ApiTags('📤 Chatting')
|
||||
export class ChattingController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@@ -45,16 +45,6 @@ export class ChattingController {
|
||||
return whatsapp.sendContactVCard(request);
|
||||
}
|
||||
|
||||
@Get('/sendText')
|
||||
@ApiOperation({ summary: 'Send a text message', deprecated: true })
|
||||
sendTextGet(@Query() query: MessageTextQuery) {
|
||||
const whatsapp = this.manager.getSession(query.session);
|
||||
const msg = new MessageTextRequest();
|
||||
msg.chatId = query.phone;
|
||||
msg.text = query.text;
|
||||
return whatsapp.sendText(msg);
|
||||
}
|
||||
|
||||
@Post('/sendText')
|
||||
@ApiOperation({ summary: 'Send a text message' })
|
||||
sendText(@Body() request: MessageTextRequest): Promise<WAMessage> {
|
||||
@@ -123,13 +113,6 @@ export class ChattingController {
|
||||
return whatsapp.sendVideo(request);
|
||||
}
|
||||
|
||||
@Post('/reply')
|
||||
@ApiOperation({ summary: 'Reply to a text message' })
|
||||
reply(@Body() request: MessageReplyRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
return whatsapp.reply(request);
|
||||
}
|
||||
|
||||
@Post('/sendSeen')
|
||||
sendSeen(@Body() chat: SendSeenRequest) {
|
||||
const whatsapp = this.manager.getSession(chat.session);
|
||||
@@ -172,4 +155,25 @@ export class ChattingController {
|
||||
const whatsapp = this.manager.getSession(query.session);
|
||||
return whatsapp.getMessages(query);
|
||||
}
|
||||
|
||||
@Get('/sendText')
|
||||
@ApiOperation({ summary: 'Send a text message', deprecated: true })
|
||||
sendTextGet(@Query() query: MessageTextQuery) {
|
||||
const whatsapp = this.manager.getSession(query.session);
|
||||
const msg = new MessageTextRequest();
|
||||
msg.chatId = query.phone;
|
||||
msg.text = query.text;
|
||||
return whatsapp.sendText(msg);
|
||||
}
|
||||
|
||||
@Post('/reply')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'DEPRECATED - you can set "reply_to" field when sending text, image, etc',
|
||||
deprecated: true,
|
||||
})
|
||||
reply(@Body() request: MessageReplyRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
return whatsapp.reply(request);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { ContactQuery, ContactRequest } from '../structures/contacts.dto';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/contacts')
|
||||
@ApiTags('contacts')
|
||||
@ApiTags('👤 Contacts')
|
||||
export class ContactsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
Put,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
@@ -18,11 +22,10 @@ import {
|
||||
SettingsSecurityChangeInfo,
|
||||
SubjectRequest,
|
||||
} from '../structures/groups.dto';
|
||||
import { SessionApiParam, SessionParam } from './helpers';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/groups')
|
||||
@ApiTags('groups')
|
||||
@ApiTags('👥 Groups')
|
||||
export class GroupsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { HealthCheck } from '@nestjs/terminus';
|
||||
|
||||
import { WAHAHealthCheckService } from '../core/abc/WAHAHealthCheckService';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('health')
|
||||
@ApiTags('other')
|
||||
@ApiTags('🔍 Observability')
|
||||
export class HealthController {
|
||||
constructor(private wahaHealth: WAHAHealthCheckService) {}
|
||||
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import {
|
||||
applyDecorators,
|
||||
Injectable,
|
||||
Param,
|
||||
PipeTransform,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiExtraModels,
|
||||
ApiParam,
|
||||
ApiResponse,
|
||||
getSchemaPath,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { Base64File } from '../structures/files.dto';
|
||||
|
||||
/**
|
||||
* Get session name and return Whatsapp session back (if exists)
|
||||
* use it as
|
||||
@Param('session', SessionPipe) session: WhatsappSession,
|
||||
*/
|
||||
@Injectable()
|
||||
export class SessionPipe implements PipeTransform<WhatsappSession> {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
async transform(value: any) {
|
||||
return this.manager.getSession(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator for a method that uses SessionPipe above
|
||||
*/
|
||||
export const SessionApiParam = ApiParam({
|
||||
name: 'session',
|
||||
required: true,
|
||||
type: 'string',
|
||||
schema: {
|
||||
default: 'default',
|
||||
},
|
||||
description: 'WhatsApp session name',
|
||||
});
|
||||
|
||||
/**
|
||||
* Session param
|
||||
@SessionParam session: WhatsappSession,
|
||||
*/
|
||||
export const SessionParam = Param('session', SessionPipe);
|
||||
|
||||
export const ChatIdApiParam = ApiParam({
|
||||
name: 'chatId',
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'Chat ID',
|
||||
example: '123456789@c.us',
|
||||
});
|
||||
|
||||
export const MessageIdApiParam = ApiParam({
|
||||
name: 'messageId',
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'Message ID',
|
||||
example: 'true_123456789@c.us_BAE6A33293978B16',
|
||||
});
|
||||
|
||||
function getRefSchemaPaths(models) {
|
||||
return models.map((model) => {
|
||||
return { $ref: getSchemaPath(model) };
|
||||
});
|
||||
}
|
||||
|
||||
export function ApiFileAcceptHeader(...models) {
|
||||
models = models.length ? models : [Base64File];
|
||||
return applyDecorators(
|
||||
// Add extra models, otherwise it'll give a error
|
||||
// $ref not found
|
||||
ApiExtraModels(...models),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
content: {
|
||||
'image/png': {
|
||||
schema: {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
},
|
||||
},
|
||||
'application/json': {
|
||||
schema: {
|
||||
oneOf: getRefSchemaPaths(models),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import { Body, Controller, Get, Param, Put } from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { SessionApiParam, SessionParam } from '@waha/api/helpers';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { Label, LabelID, SetLabelsRequest } from '@waha/structures/labels.dto';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/labels')
|
||||
@ApiTags('labels')
|
||||
@ApiTags('🏷️ Labels')
|
||||
export class LabelsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { PingResponse } from '@waha/structures/ping.dto';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('ping')
|
||||
@ApiTags('other')
|
||||
@ApiTags('🔍 Observability')
|
||||
export class PingController {
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Ping the server, perform no operations.' })
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
@@ -15,11 +19,10 @@ import {
|
||||
WAHAChatPresences,
|
||||
WAHASessionPresence,
|
||||
} from '../structures/presence.dto';
|
||||
import { SessionApiParam, SessionParam } from './helpers';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/presence')
|
||||
@ApiTags('presence')
|
||||
@ApiTags('✅ Presence')
|
||||
export class PresenceController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -7,16 +7,16 @@ import {
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
|
||||
import { Response } from 'express';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
|
||||
import { SessionQuery } from '../structures/base.dto';
|
||||
import { BufferResponseInterceptor } from './BufferResponseInterceptor';
|
||||
import { ApiFileAcceptHeader } from './helpers';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api')
|
||||
@ApiTags('screenshot')
|
||||
@ApiTags('🖼️ Screenshot')
|
||||
export class ScreenshotController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import * as process from 'node:process';
|
||||
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Logger,
|
||||
Post,
|
||||
Query,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { getApp } from '@waha/main';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { WAHAEnvironment } from '@waha/structures/environment.dto';
|
||||
import {
|
||||
EnvironmentQuery,
|
||||
ServerStatusResponse,
|
||||
StopRequest,
|
||||
} from '@waha/structures/server.dto';
|
||||
import { VERSION } from '@waha/version';
|
||||
import * as lodash from 'lodash';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/server')
|
||||
@ApiTags('🔍 Observability')
|
||||
export class ServerController {
|
||||
private logger: Logger;
|
||||
|
||||
constructor() {
|
||||
this.logger = new Logger('ServerController');
|
||||
}
|
||||
|
||||
@Get('version')
|
||||
@ApiOperation({ summary: 'Get the version of the server' })
|
||||
get(): WAHAEnvironment {
|
||||
return VERSION;
|
||||
}
|
||||
|
||||
@Get('environment')
|
||||
@ApiOperation({ summary: 'Return the server environment' })
|
||||
environment(
|
||||
@Query(new WAHAValidationPipe()) query: EnvironmentQuery,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
): object {
|
||||
let result = process.env;
|
||||
if (!query.all) {
|
||||
result = lodash.pickBy(result, (value, key) => {
|
||||
return (
|
||||
key.startsWith('WAHA_') ||
|
||||
key.startsWith('WHATSAPP_') ||
|
||||
key === 'DEBUG'
|
||||
);
|
||||
});
|
||||
}
|
||||
const map = new Map<string, string>();
|
||||
// sort and set
|
||||
Object.keys(result)
|
||||
.sort()
|
||||
.forEach((key) => {
|
||||
map.set(key, result[key]);
|
||||
});
|
||||
return Object.fromEntries(map);
|
||||
}
|
||||
|
||||
@Get('status')
|
||||
@ApiOperation({ summary: 'The server status' })
|
||||
async status(): Promise<ServerStatusResponse> {
|
||||
const now = Date.now();
|
||||
const uptime = Math.floor(process.uptime() * 1000);
|
||||
const startTimestamp = now - uptime;
|
||||
return {
|
||||
startTimestamp: startTimestamp,
|
||||
uptime: uptime,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('stop')
|
||||
@ApiOperation({
|
||||
summary: 'Stop (and restart) the server',
|
||||
description:
|
||||
"If you're using docker, after calling this endpoint Docker will start a new container, " +
|
||||
'so you can use this endpoint to restart the server',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async stop(@Body() request: StopRequest) {
|
||||
const timeout = 1_000;
|
||||
if (request.force) {
|
||||
this.logger.log(`Force stopping the server in ${timeout}ms`);
|
||||
setTimeout(() => {
|
||||
this.logger.log('Force stopping the server');
|
||||
process.kill(process.pid, 'SIGKILL');
|
||||
}, timeout);
|
||||
} else {
|
||||
this.logger.log(`Gracefully stopping the server in ${timeout}ms`);
|
||||
setTimeout(async () => {
|
||||
this.logger.log('Gracefully closing the application...');
|
||||
const app = getApp();
|
||||
if (app) {
|
||||
await app.close();
|
||||
}
|
||||
this.logger.log('Application closed');
|
||||
process.exit(0);
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
+255
-47
@@ -6,69 +6,66 @@ import {
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiExtraModels,
|
||||
ApiOperation,
|
||||
ApiSecurity,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { WAHASessionStatus } from '@waha/structures/enums.dto';
|
||||
import {
|
||||
SessionLogoutDeprecatedRequest,
|
||||
SessionStartDeprecatedRequest,
|
||||
SessionStopDeprecatedRequest,
|
||||
} from '@waha/structures/sessions.deprecated.dto';
|
||||
import { generatePrefixedId } from '@waha/utils/ids';
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { parseBool } from '../helpers';
|
||||
import {
|
||||
ListSessionsQuery,
|
||||
MeInfo,
|
||||
SessionCreateRequest,
|
||||
SessionDTO,
|
||||
SessionInfo,
|
||||
SessionLogoutRequest,
|
||||
SessionStartRequest,
|
||||
SessionStopRequest,
|
||||
SessionUpdateRequest,
|
||||
} from '../structures/sessions.dto';
|
||||
import { SessionApiParam, SessionParam } from './helpers';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const AsyncLock = require('async-lock');
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/sessions')
|
||||
@ApiTags('sessions')
|
||||
@ApiTags('🖥️ Sessions')
|
||||
class SessionsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
private lock: any;
|
||||
|
||||
@Post('/start/')
|
||||
async start(@Body() request: SessionStartRequest): Promise<SessionDTO> {
|
||||
const result = await this.manager.start(request);
|
||||
await this.manager.sessionConfigRepository.save(
|
||||
request.name,
|
||||
request.config || null,
|
||||
);
|
||||
return result;
|
||||
constructor(private manager: SessionManager) {
|
||||
this.lock = new AsyncLock({ maxPending: Infinity });
|
||||
}
|
||||
|
||||
@Post('/stop/')
|
||||
@ApiOperation({ summary: 'Stop session' })
|
||||
async stop(@Body() request: SessionStopRequest): Promise<void> {
|
||||
if (request.logout) {
|
||||
await this.manager.logout(request);
|
||||
} else {
|
||||
await this.manager.stop(request);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@Post('/logout/')
|
||||
@ApiOperation({ summary: 'Logout from session.' })
|
||||
clean(@Body() request: SessionLogoutRequest): Promise<void> {
|
||||
return this.manager.logout(request);
|
||||
private withLock(name: string, fn: () => any) {
|
||||
return this.lock.acquire(name, fn);
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
list(@Query() query: ListSessionsQuery): Promise<SessionInfo[]> {
|
||||
const all = parseBool(query.all);
|
||||
return this.manager.getSessions(all);
|
||||
@ApiOperation({ summary: 'List all sessions' })
|
||||
list(
|
||||
@Query(new WAHAValidationPipe()) query: ListSessionsQuery,
|
||||
): Promise<SessionInfo[]> {
|
||||
return this.manager.getSessions(query.all);
|
||||
}
|
||||
|
||||
@Get('/:session')
|
||||
@ApiOperation({ summary: 'Get session information' })
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async get(@Param('session') name: string): Promise<SessionInfo> {
|
||||
const session = this.manager.getSessionInfo(name);
|
||||
if (session === null) {
|
||||
@@ -76,20 +73,231 @@ class SessionsController {
|
||||
}
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/sessions/:session')
|
||||
@ApiTags('sessions')
|
||||
class SessionController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('me')
|
||||
@Get(':session/me')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get information about the authenticated account' })
|
||||
getMe(@SessionParam session: WhatsappSession): MeInfo | null {
|
||||
return session.getSessionMeInfo();
|
||||
}
|
||||
|
||||
@Post('')
|
||||
@ApiOperation({
|
||||
summary: 'Create a session',
|
||||
description:
|
||||
'Create session a new session (and start it at the same time if required).',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async create(@Body() request: SessionCreateRequest): Promise<SessionDTO> {
|
||||
const name = request.name || generatePrefixedId('session');
|
||||
await this.withLock(name, async () => {
|
||||
if (await this.manager.exists(name)) {
|
||||
const msg = `Session '${name}' already exists. Use PUT to update it.`;
|
||||
throw new UnprocessableEntityException(msg);
|
||||
}
|
||||
|
||||
const config = request.config;
|
||||
const start = request.start || false;
|
||||
await this.manager.upsert(name, config);
|
||||
if (start) {
|
||||
await this.manager.start(name);
|
||||
}
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
}
|
||||
|
||||
@Put(':session')
|
||||
@ApiOperation({
|
||||
summary: 'Update a session',
|
||||
description: '',
|
||||
})
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async update(
|
||||
@Param('session') name: string,
|
||||
@Body() request: SessionUpdateRequest,
|
||||
): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
if (!(await this.manager.exists(name))) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
const config = request.config;
|
||||
const isRunning = this.manager.isRunning(name);
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.upsert(name, config);
|
||||
if (isRunning) {
|
||||
await this.manager.start(name);
|
||||
}
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
}
|
||||
|
||||
@Delete(':session')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Delete the session',
|
||||
description:
|
||||
'Delete the session with the given name. Stop and logout as well. Idempotent operation.',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async delete(@Param('session') name: string): Promise<void> {
|
||||
await this.withLock(name, async () => {
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.logout(name);
|
||||
await this.manager.delete(name);
|
||||
});
|
||||
}
|
||||
|
||||
@Post(':session/start')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Start the session',
|
||||
description:
|
||||
'Start the session with the given name. The session must exist. Identity operation.',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async start(@Param('session') name: string): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
const exists = await this.manager.exists(name);
|
||||
if (!exists) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
const isRunning = this.manager.isRunning(name);
|
||||
if (!isRunning) {
|
||||
await this.manager.start(name);
|
||||
}
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
}
|
||||
|
||||
@Post(':session/stop')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Stop the session',
|
||||
description: 'Stop the session with the given name. Idempotent operation.',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async stop(@Param('session') name: string): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
const exists = await this.manager.exists(name);
|
||||
if (!exists) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
await this.manager.stop(name, false);
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
}
|
||||
|
||||
@Post(':session/logout')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Logout from the session',
|
||||
description: 'Logout the session, restart a session if it was not STOPPED',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async logout(@Param('session') name: string): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
const exists = await this.manager.exists(name);
|
||||
if (!exists) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
const isRunning = this.manager.isRunning(name);
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.logout(name);
|
||||
if (isRunning) {
|
||||
await this.manager.start(name);
|
||||
}
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
}
|
||||
|
||||
@Post(':session/restart')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Restart the session',
|
||||
description: 'Restart the session with the given name.',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async restart(@Param('session') name: string): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
const exists = await this.manager.exists(name);
|
||||
if (!exists) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.start(name);
|
||||
});
|
||||
|
||||
return await this.manager.getSessionInfo(name);
|
||||
}
|
||||
|
||||
@Post('/start/')
|
||||
@ApiOperation({
|
||||
summary: 'Upsert and Start session',
|
||||
description:
|
||||
'Create session (if not exists) or update a config (if exists) and start it.',
|
||||
deprecated: true,
|
||||
})
|
||||
async DEPRACATED_start(
|
||||
@Body() request: SessionStartDeprecatedRequest,
|
||||
): Promise<SessionDTO> {
|
||||
const name = request.name;
|
||||
if (this.manager.isRunning(name)) {
|
||||
const msg = `Session '${name}' is already started.`;
|
||||
throw new UnprocessableEntityException(msg);
|
||||
}
|
||||
|
||||
return await this.withLock(name, async () => {
|
||||
const config = request.config;
|
||||
if (config) {
|
||||
await this.manager.upsert(name, config);
|
||||
}
|
||||
return await this.manager.start(name);
|
||||
});
|
||||
}
|
||||
|
||||
@Post('/stop/')
|
||||
@ApiOperation({
|
||||
summary: 'Stop (and Logout if asked) session',
|
||||
description: 'Stop session and Logout by default.',
|
||||
deprecated: true,
|
||||
})
|
||||
async DEPRECATED_stop(
|
||||
@Body() request: SessionStopDeprecatedRequest,
|
||||
): Promise<void> {
|
||||
const name = request.name;
|
||||
if (request.logout) {
|
||||
// Old API did remove the session complete
|
||||
await this.withLock(name, async () => {
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.logout(name);
|
||||
await this.manager.delete(name);
|
||||
});
|
||||
} else {
|
||||
await this.withLock(name, async () => {
|
||||
await this.manager.stop(name, false);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@Post('/logout/')
|
||||
@ApiOperation({
|
||||
summary: 'Logout and Delete session.',
|
||||
description: 'Stop, Logout and Delete session.',
|
||||
deprecated: true,
|
||||
})
|
||||
async DEPRECATED_logout(
|
||||
@Body() request: SessionLogoutDeprecatedRequest,
|
||||
): Promise<void> {
|
||||
const name = request.name;
|
||||
await this.withLock(name, async () => {
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.logout(name);
|
||||
await this.manager.delete(name);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export { SessionController, SessionsController };
|
||||
export { SessionsController };
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
@@ -18,11 +22,10 @@ import {
|
||||
VideoStatus,
|
||||
VoiceStatus,
|
||||
} from '../structures/status.dto';
|
||||
import { SessionApiParam, SessionParam } from './helpers';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/status')
|
||||
@ApiTags('status')
|
||||
@ApiTags('🟢 Status')
|
||||
class StatusController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ import { VERSION } from '../version';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/version')
|
||||
@ApiTags('other')
|
||||
@ApiTags('🔍 Observability')
|
||||
export class VersionController {
|
||||
@Get('')
|
||||
@ApiOperation({ summary: 'Get the version of the server' })
|
||||
@ApiOperation({ summary: 'Get the version of the server', deprecated: true })
|
||||
get(): WAHAEnvironment {
|
||||
return VERSION;
|
||||
}
|
||||
|
||||
+9
-19
@@ -1,18 +1,16 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
|
||||
import { parseBool } from './helpers';
|
||||
import { WebhookConfig } from './structures/webhooks.config.dto';
|
||||
|
||||
@Injectable()
|
||||
export class WhatsappConfigService {
|
||||
public filesUri = '/api/files';
|
||||
public schema = 'http';
|
||||
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
get filesURL(): string {
|
||||
return `${this.schema}://${this.hostname}:${this.port}${this.filesUri}/`;
|
||||
get schema() {
|
||||
return this.configService.get('WHATSAPP_API_SCHEMA', 'http');
|
||||
}
|
||||
|
||||
get hostname(): string {
|
||||
@@ -23,17 +21,6 @@ export class WhatsappConfigService {
|
||||
return this.configService.get('WHATSAPP_API_PORT', '3000');
|
||||
}
|
||||
|
||||
get filesFolder(): string {
|
||||
return this.configService.get(
|
||||
'WHATSAPP_FILES_FOLDER',
|
||||
'/tmp/whatsapp-files',
|
||||
);
|
||||
}
|
||||
|
||||
get filesLifetime(): number {
|
||||
return this.configService.get<number>('WHATSAPP_FILES_LIFETIME', 180);
|
||||
}
|
||||
|
||||
get mimetypes(): string[] {
|
||||
if (!this.shouldDownloadMedia) {
|
||||
return ['mimetype/ignore-all-media'];
|
||||
@@ -64,8 +51,11 @@ export class WhatsappConfigService {
|
||||
}
|
||||
|
||||
get proxyServer(): string[] | string | undefined {
|
||||
const single = this.configService.get('WHATSAPP_PROXY_SERVER', undefined);
|
||||
const multipleValues = this.configService.get(
|
||||
const single = this.configService.get<string>(
|
||||
'WHATSAPP_PROXY_SERVER',
|
||||
undefined,
|
||||
);
|
||||
const multipleValues = this.configService.get<string>(
|
||||
'WHATSAPP_PROXY_SERVER_LIST',
|
||||
undefined,
|
||||
);
|
||||
@@ -101,7 +91,7 @@ export class WhatsappConfigService {
|
||||
return this.get('WHATSAPP_HOOK_URL');
|
||||
}
|
||||
|
||||
private getWebhookEvents(): string[] {
|
||||
private getWebhookEvents(): WAHAEvents[] {
|
||||
const value = this.get('WHATSAPP_HOOK_EVENTS', '');
|
||||
return value ? value.split(',') : [];
|
||||
}
|
||||
|
||||
@@ -56,29 +56,30 @@ export class SwaggerConfiguratorCore {
|
||||
.setDescription(this.description)
|
||||
.setExternalDoc(this.title, this.externalDocUrl)
|
||||
.setVersion(VERSION.version)
|
||||
.addTag('sessions', 'Control WhatsApp sessions')
|
||||
.addTag('auth', 'Authentication')
|
||||
.addTag('screenshot', 'Get screenshot of WhatsApp and show QR code')
|
||||
.addTag('chatting', 'Chatting methods')
|
||||
.addTag('channels', 'Channels (newsletters) methods')
|
||||
.addTag('🖥️ Sessions', 'Control WhatsApp sessions (accounts)')
|
||||
.addTag('🔑 Auth', 'Authentication')
|
||||
.addTag('🖼️ Screenshot', 'Get screenshot of WhatsApp and show QR code')
|
||||
.addTag('📤 Chatting', 'Chatting methods')
|
||||
.addTag('📢 Channels', 'Channels (newsletters) methods')
|
||||
.addTag(
|
||||
'status',
|
||||
'🟢 Status',
|
||||
'Status (aka stories) methods. <b>NOWEB</b> engine only!',
|
||||
)
|
||||
.addTag('chats', `Chats methods`)
|
||||
.addTag('💬 Chats', `Chats methods`)
|
||||
.addTag(
|
||||
'contacts',
|
||||
'👤 Contacts',
|
||||
`Contacts methods.<br>
|
||||
Use phone number (without +) or phone number and \`@c.us\` at the end as \`contactId\`.<br>
|
||||
'E.g: \`12312312310\` OR \`12312312310@c.us\`<br>`,
|
||||
)
|
||||
.addTag('groups', `Groups methods.<br>`)
|
||||
.addTag('presence', `Presence information`)
|
||||
.addTag('👥 Groups', `Groups methods.<br>`)
|
||||
.addTag('✅ Presence', `Presence information`)
|
||||
.addTag(
|
||||
'labels',
|
||||
'🏷️ Labels',
|
||||
'Labels - available only for WhatsApp Business accounts',
|
||||
)
|
||||
.addTag('other', 'Other methods')
|
||||
.addTag('🔍 Observability', 'Other methods')
|
||||
.addTag('🗄️ Storage', 'Storage methods')
|
||||
.addApiKey({
|
||||
type: 'apiKey',
|
||||
description: 'Your secret api key',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ConsoleLogger, Injectable } from '@nestjs/common';
|
||||
import { DiskHealthIndicator, HealthCheckService } from '@nestjs/terminus';
|
||||
import { Injectable, Logger, LoggerService } from '@nestjs/common';
|
||||
import { HealthCheckService } from '@nestjs/terminus';
|
||||
import type { HealthCheckResult } from '@nestjs/terminus/dist/health-check/health-check-result.interface';
|
||||
|
||||
import { WhatsappConfigService } from '../../config.service';
|
||||
@@ -7,12 +7,14 @@ import { SessionManager } from './manager.abc';
|
||||
|
||||
@Injectable()
|
||||
export abstract class WAHAHealthCheckService {
|
||||
protected logger: LoggerService;
|
||||
constructor(
|
||||
protected sessionManager: SessionManager,
|
||||
protected health: HealthCheckService,
|
||||
protected log: ConsoleLogger,
|
||||
protected config: WhatsappConfigService,
|
||||
) {}
|
||||
) {
|
||||
this.logger = new Logger('WAHAHealthCheckService');
|
||||
}
|
||||
|
||||
abstract check(): Promise<HealthCheckResult>;
|
||||
}
|
||||
+17
-11
@@ -1,19 +1,13 @@
|
||||
import {
|
||||
BeforeApplicationShutdown,
|
||||
OnApplicationShutdown,
|
||||
} from '@nestjs/common';
|
||||
import { BeforeApplicationShutdown } from '@nestjs/common';
|
||||
import { WAHAWebhook } from '@waha/structures/webhooks.dto';
|
||||
import { VERSION } from '@waha/version';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { WAHAEngine, WAHAEvents } from '../../structures/enums.dto';
|
||||
import {
|
||||
MeInfo,
|
||||
SessionConfig,
|
||||
SessionDTO,
|
||||
SessionInfo,
|
||||
SessionLogoutRequest,
|
||||
SessionStartRequest,
|
||||
SessionStopRequest,
|
||||
} from '../../structures/sessions.dto';
|
||||
import { ISessionAuthRepository } from '../storage/ISessionAuthRepository';
|
||||
import { ISessionConfigRepository } from '../storage/ISessionConfigRepository';
|
||||
@@ -37,11 +31,22 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
|
||||
//
|
||||
// API Methods
|
||||
//
|
||||
abstract start(request: SessionStartRequest): Promise<SessionDTO>;
|
||||
/**
|
||||
* Either create or update
|
||||
*/
|
||||
abstract exists(name: string): Promise<boolean>;
|
||||
|
||||
abstract stop(request: SessionStopRequest): Promise<void>;
|
||||
abstract isRunning(name: string): boolean;
|
||||
|
||||
abstract logout(request: SessionLogoutRequest): Promise<void>;
|
||||
abstract upsert(name: string, config?: SessionConfig): Promise<void>;
|
||||
|
||||
abstract delete(name: string): Promise<void>;
|
||||
|
||||
abstract start(name: string): Promise<SessionDTO>;
|
||||
|
||||
abstract stop(name: string, silent: boolean): Promise<void>;
|
||||
|
||||
abstract logout(name: string): Promise<void>;
|
||||
|
||||
abstract getSession(name: string): WhatsappSession;
|
||||
|
||||
@@ -55,6 +60,7 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
|
||||
const data: WAHAWebhook = {
|
||||
event: event,
|
||||
session: session.name,
|
||||
metadata: session.sessionConfig?.metadata,
|
||||
me: me,
|
||||
payload: payload,
|
||||
engine: session.engine,
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
abstract class MediaStorage {
|
||||
abstract save(
|
||||
messageId: string,
|
||||
mimetype: string,
|
||||
buffer: Buffer,
|
||||
): Promise<string>;
|
||||
}
|
||||
|
||||
interface IEngineMediaProcessor<Message> {
|
||||
hasMedia(message: Message): boolean;
|
||||
|
||||
getFilename(message: Message): string | null;
|
||||
|
||||
getMimetype(message: Message): string;
|
||||
|
||||
getMessageId(message: Message): string;
|
||||
|
||||
getMediaBuffer(message: Message): Promise<Buffer | null>;
|
||||
}
|
||||
|
||||
interface MediaManager {
|
||||
processMedia<Message>(
|
||||
processor: IEngineMediaProcessor<Message>,
|
||||
message: Message,
|
||||
): Promise<Message>;
|
||||
}
|
||||
|
||||
export { IEngineMediaProcessor, MediaManager, MediaStorage };
|
||||
+33
-19
@@ -56,9 +56,9 @@ import {
|
||||
} from '../../structures/status.dto';
|
||||
import { WASessionStatusBody } from '../../structures/webhooks.dto';
|
||||
import { NotImplementedByEngineError } from '../exceptions';
|
||||
import { IMediaManager } from '../media/IMediaManager';
|
||||
import { QR } from '../QR';
|
||||
import { DataStore } from './DataStore';
|
||||
import { MediaManager } from './media.abc';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const qrcode = require('qrcode-terminal');
|
||||
@@ -88,7 +88,7 @@ export enum WAHAInternalEvent {
|
||||
export interface SessionParams {
|
||||
name: string;
|
||||
printQR: boolean;
|
||||
mediaManager: MediaManager;
|
||||
mediaManager: IMediaManager;
|
||||
loggerBuilder: LoggerBuilder;
|
||||
sessionStore: DataStore;
|
||||
proxyConfig?: ProxyConfig;
|
||||
@@ -101,7 +101,7 @@ export abstract class WhatsappSession {
|
||||
public engine: WAHAEngine;
|
||||
|
||||
public name: string;
|
||||
protected mediaManager: MediaManager;
|
||||
protected mediaManager: IMediaManager;
|
||||
public loggerBuilder: LoggerBuilder;
|
||||
protected logger: Logger;
|
||||
protected sessionStore: DataStore;
|
||||
@@ -152,29 +152,43 @@ export abstract class WhatsappSession {
|
||||
// Run optimized version of Chrome
|
||||
// References:
|
||||
// https://github.com/pedroslopez/whatsapp-web.js/issues/1420
|
||||
// https://github.com/wppconnect-team/wppconnect/issues/1326
|
||||
// https://www.bannerbear.com/blog/ways-to-speed-up-puppeteer-screenshots/
|
||||
return [
|
||||
'--no-sandbox',
|
||||
'--aggressive-cache-discard',
|
||||
'--disable-accelerated-2d-canvas',
|
||||
'--disable-application-cache',
|
||||
'--disable-background-networking',
|
||||
'--disable-cache',
|
||||
'--disable-client-side-phishing-detection',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-component-update',
|
||||
'--disable-default-apps',
|
||||
'--disable-popup-blocking',
|
||||
'--disable-offer-store-unmasked-wallet-cards',
|
||||
'--disable-speech-api',
|
||||
'--hide-scrollbars',
|
||||
'--mute-audio',
|
||||
'--disable-extensions',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-accelerated-2d-canvas',
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--no-pings',
|
||||
'--password-store=basic',
|
||||
'--use-mock-keychain',
|
||||
'--no-zygote',
|
||||
'--single-process',
|
||||
'--disable-extensions',
|
||||
'--disable-gpu',
|
||||
'--disable-offer-store-unmasked-wallet-cards',
|
||||
'--disable-offline-load-stale-cache',
|
||||
'--disable-popup-blocking',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-speech-api',
|
||||
'--disable-sync',
|
||||
'--disable-translate',
|
||||
'--disable-web-security',
|
||||
'--disk-cache-size=0',
|
||||
'--hide-scrollbars',
|
||||
'--ignore-certificate-errors',
|
||||
'--ignore-ssl-errors',
|
||||
'--metrics-recording-only',
|
||||
'--mute-audio',
|
||||
'--no-default-browser-check',
|
||||
'--no-first-run',
|
||||
'--no-pings',
|
||||
'--no-sandbox',
|
||||
'--no-zygote',
|
||||
'--password-store=basic',
|
||||
'--safebrowsing-disable-auto-update',
|
||||
'--single-process',
|
||||
'--use-mock-keychain',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
BeforeApplicationShutdown,
|
||||
ConsoleLogger,
|
||||
Logger,
|
||||
LoggerService,
|
||||
} from '@nestjs/common';
|
||||
@@ -13,9 +12,10 @@ import {
|
||||
WebSocketServer,
|
||||
} from '@nestjs/websockets';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WebsocketHeartbeatJob } from '@waha/nestjs/ws/WebsocketHeartbeatJob';
|
||||
import { WebSocket } from '@waha/nestjs/ws/ws';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
import { WebsocketHeartbeatJob } from '@waha/utils/WebsocketHeartbeatJob';
|
||||
import { WebSocket } from '@waha/utils/ws';
|
||||
import { generatePrefixedId } from '@waha/utils/ids';
|
||||
import { IncomingMessage } from 'http';
|
||||
import * as lodash from 'lodash';
|
||||
import * as url from 'url';
|
||||
@@ -52,8 +52,8 @@ export class WebsocketGatewayCore
|
||||
}
|
||||
|
||||
handleConnection(socket: WebSocket, request: IncomingMessage, ...args): any {
|
||||
const random = Math.random().toString(36).substring(7);
|
||||
const id = `wsc_${random}`;
|
||||
// wsc - websocket client
|
||||
const id = generatePrefixedId('wsc');
|
||||
socket.id = id;
|
||||
this.logger.debug(`New client connected: ${request.url}`);
|
||||
const params = this.getParams(id, request, socket);
|
||||
@@ -105,7 +105,9 @@ export class WebsocketGatewayCore
|
||||
this.logger.log('Shutting down websocket server');
|
||||
// Allow pending messages to be sent, it can be even 1ms, just to release the event loop
|
||||
await sleep(100);
|
||||
this.server.clients.forEach((options, client) => {
|
||||
this.server?.clients.forEach((options, client) => {
|
||||
// @ts-ignore
|
||||
this.logger.debug(`Closing client connection - ${client.id}...`);
|
||||
client.close(1001, 'Server is shutting down');
|
||||
});
|
||||
// Do not turn off heartbeat service here,
|
||||
|
||||
+32
-29
@@ -1,17 +1,20 @@
|
||||
import { ConsoleLogger, INestApplication, Module } from '@nestjs/common';
|
||||
import { INestApplication, Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { TerminusModule } from '@nestjs/terminus';
|
||||
import { BufferJsonReplacerInterceptor } from '@waha/api/BufferJsonReplacerInterceptor';
|
||||
import { ChannelsController } from '@waha/api/channels.controller';
|
||||
import { ServerController } from '@waha/api/server.controller';
|
||||
import { WebsocketGatewayCore } from '@waha/core/api/websocket.gateway.core';
|
||||
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
|
||||
import { BufferJsonReplacerInterceptor } from '@waha/nestjs/BufferJsonReplacerInterceptor';
|
||||
import {
|
||||
getPinoHttpUseLevel,
|
||||
getPinoLogLevel,
|
||||
getPinoTransport,
|
||||
} from '@waha/utils/logging';
|
||||
import * as Joi from 'joi';
|
||||
import { LoggerModule } from 'nestjs-pino';
|
||||
import { join } from 'path';
|
||||
import { Logger } from 'pino';
|
||||
@@ -26,10 +29,7 @@ import { LabelsController } from '../api/labels.controller';
|
||||
import { PingController } from '../api/ping.controller';
|
||||
import { PresenceController } from '../api/presence.controller';
|
||||
import { ScreenshotController } from '../api/screenshot.controller';
|
||||
import {
|
||||
SessionController,
|
||||
SessionsController,
|
||||
} from '../api/sessions.controller';
|
||||
import { SessionsController } from '../api/sessions.controller';
|
||||
import { StatusController } from '../api/status.controller';
|
||||
import { VersionController } from '../api/version.controller';
|
||||
import { WhatsappConfigService } from '../config.service';
|
||||
@@ -51,7 +51,11 @@ export const IMPORTS = [
|
||||
transport: getPinoTransport(),
|
||||
autoLogging: {
|
||||
ignore: (req) => {
|
||||
return req.url.startsWith('/dashboard/');
|
||||
return (
|
||||
req.url.startsWith('/dashboard/') ||
|
||||
req.url.startsWith('/api/files/') ||
|
||||
req.url.startsWith('/api/s3/')
|
||||
);
|
||||
},
|
||||
},
|
||||
serializers: {
|
||||
@@ -70,29 +74,24 @@ export const IMPORTS = [
|
||||
}),
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
validationSchema: Joi.object({
|
||||
WHATSAPP_API_SCHEMA: Joi.string().valid('http', 'https').default('http'),
|
||||
}),
|
||||
}),
|
||||
ServeStaticModule.forRootAsync({
|
||||
imports: [],
|
||||
extraProviders: [WhatsappConfigService, DashboardConfigServiceCore],
|
||||
inject: [WhatsappConfigService, DashboardConfigServiceCore],
|
||||
useFactory: (
|
||||
config: WhatsappConfigService,
|
||||
dashboardConfig: DashboardConfigServiceCore,
|
||||
) => {
|
||||
const options = [
|
||||
// Serve files (media)
|
||||
extraProviders: [DashboardConfigServiceCore],
|
||||
inject: [DashboardConfigServiceCore],
|
||||
useFactory: (dashboardConfig: DashboardConfigServiceCore) => {
|
||||
if (!dashboardConfig.enabled) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
rootPath: config.filesFolder,
|
||||
serveRoot: config.filesUri,
|
||||
},
|
||||
];
|
||||
if (dashboardConfig.enabled) {
|
||||
options.push({
|
||||
rootPath: join(__dirname, '..', 'dashboard'),
|
||||
serveRoot: dashboardConfig.dashboardUri,
|
||||
});
|
||||
}
|
||||
return options;
|
||||
},
|
||||
];
|
||||
},
|
||||
}),
|
||||
PassportModule,
|
||||
@@ -101,7 +100,6 @@ export const IMPORTS = [
|
||||
export const CONTROLLERS = [
|
||||
AuthController,
|
||||
SessionsController,
|
||||
SessionController,
|
||||
ChattingController,
|
||||
ChatsController,
|
||||
ChannelsController,
|
||||
@@ -111,9 +109,10 @@ export const CONTROLLERS = [
|
||||
GroupsController,
|
||||
PresenceController,
|
||||
ScreenshotController,
|
||||
VersionController,
|
||||
HealthController,
|
||||
PingController,
|
||||
HealthController,
|
||||
ServerController,
|
||||
VersionController,
|
||||
];
|
||||
const PROVIDERS = [
|
||||
{
|
||||
@@ -132,8 +131,8 @@ const PROVIDERS = [
|
||||
SwaggerConfigServiceCore,
|
||||
WhatsappConfigService,
|
||||
EngineConfigService,
|
||||
ConsoleLogger,
|
||||
WebsocketGatewayCore,
|
||||
MediaLocalStorageConfig,
|
||||
];
|
||||
|
||||
@Module({
|
||||
@@ -142,7 +141,11 @@ const PROVIDERS = [
|
||||
providers: PROVIDERS,
|
||||
})
|
||||
export class AppModuleCore {
|
||||
constructor(protected config: WhatsappConfigService) {}
|
||||
public startTimestamp: number;
|
||||
|
||||
constructor(protected config: WhatsappConfigService) {
|
||||
this.startTimestamp = Date.now();
|
||||
}
|
||||
|
||||
static getHttpsOptions(logger: Logger) {
|
||||
return undefined;
|
||||
|
||||
@@ -4,6 +4,7 @@ import makeWASocket, {
|
||||
DisconnectReason,
|
||||
extractMessageContent,
|
||||
getAggregateVotesInPollMessage,
|
||||
getContentType,
|
||||
getKeyAuthor,
|
||||
getUrlFromDirectPath,
|
||||
isJidGroup,
|
||||
@@ -25,6 +26,7 @@ import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
|
||||
import { Logger as BaileysLogger } from '@adiwajshing/baileys/node_modules/pino';
|
||||
import { UnprocessableEntityException } from '@nestjs/common';
|
||||
import { NowebInMemoryStore } from '@waha/core/engines/noweb/store/NowebInMemoryStore';
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { flipObject, parseBool, splitAt } from '@waha/helpers';
|
||||
import { PairingCodeResponse } from '@waha/structures/auth.dto';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
@@ -41,11 +43,13 @@ import {
|
||||
LabelChatAssociation,
|
||||
LabelID,
|
||||
} from '@waha/structures/labels.dto';
|
||||
import { ReplyToMessage } from '@waha/structures/message.dto';
|
||||
import {
|
||||
PollVote,
|
||||
PollVotePayload,
|
||||
WAMessageAckBody,
|
||||
} from '@waha/structures/webhooks.dto';
|
||||
import { waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner';
|
||||
import { SinglePeriodicJobRunner } from '@waha/utils/SinglePeriodicJobRunner';
|
||||
import * as Buffer from 'buffer';
|
||||
@@ -101,7 +105,6 @@ import {
|
||||
DeleteStatusRequest,
|
||||
TextStatus,
|
||||
} from '../../../structures/status.dto';
|
||||
import { IEngineMediaProcessor } from '../../abc/media.abc';
|
||||
import {
|
||||
ensureSuffix,
|
||||
getChannelInviteLink,
|
||||
@@ -195,9 +198,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
);
|
||||
}
|
||||
|
||||
start() {
|
||||
async start() {
|
||||
this.status = WAHASessionStatus.STARTING;
|
||||
this.buildClient();
|
||||
await this.buildClient();
|
||||
}
|
||||
|
||||
getSocketConfig(agent, state): any {
|
||||
@@ -443,8 +446,11 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
// @ts-ignore
|
||||
this.sock?.ev?.removeAllListeners();
|
||||
this.sock?.ws?.removeAllListeners();
|
||||
await this.store?.close();
|
||||
// wait until connection is not connecting to avoid error:
|
||||
// "WebSocket was closed before the connection was established"
|
||||
await waitUntil(async () => !this.sock?.ws?.isConnecting, 1_000, 10_000);
|
||||
this.sock?.end(undefined);
|
||||
this.store?.close();
|
||||
}
|
||||
|
||||
getSessionMeInfo(): MeInfo | null {
|
||||
@@ -532,26 +538,20 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
};
|
||||
}
|
||||
|
||||
sendText(request: MessageTextRequest) {
|
||||
async sendText(request: MessageTextRequest) {
|
||||
const chatId = toJID(this.ensureSuffix(request.chatId));
|
||||
const message = {
|
||||
text: request.text,
|
||||
mentions: request.mentions?.map(toJID),
|
||||
};
|
||||
return this.sock.sendMessage(chatId, message);
|
||||
const options = await this.getMessageOptions(request);
|
||||
return this.sock.sendMessage(chatId, message, options);
|
||||
}
|
||||
|
||||
public deleteMessage(chatId: string, messageId: string) {
|
||||
const jid = toJID(this.ensureSuffix(chatId));
|
||||
const key = parseMessageId(messageId);
|
||||
const options = {
|
||||
statusJidList: [
|
||||
'79521562380@s.whatsapp.net',
|
||||
'79069571990@s.whatsapp.net',
|
||||
'79069471990@s.whatsapp.net',
|
||||
],
|
||||
};
|
||||
return this.sock.sendMessage(jid, { delete: key }, options);
|
||||
const key = parseMessageIdSerialized(messageId);
|
||||
return this.sock.sendMessage(jid, { delete: key });
|
||||
}
|
||||
|
||||
public editMessage(
|
||||
@@ -560,7 +560,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
request: EditMessageRequest,
|
||||
) {
|
||||
const jid = toJID(this.ensureSuffix(chatId));
|
||||
const key = parseMessageId(messageId);
|
||||
const key = parseMessageIdSerialized(messageId);
|
||||
const message = {
|
||||
text: request.text,
|
||||
mentions: request.mentions?.map(toJID),
|
||||
@@ -590,23 +590,18 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
};
|
||||
const message = { poll: poll };
|
||||
const remoteJid = toJID(request.chatId);
|
||||
const result = await this.sock.sendMessage(remoteJid, message);
|
||||
const options = await this.getMessageOptions(request);
|
||||
const result = await this.sock.sendMessage(remoteJid, message, options);
|
||||
return this.toWAMessage(result);
|
||||
}
|
||||
|
||||
async reply(request: MessageReplyRequest) {
|
||||
const { id } = parseMessageId(request.reply_to);
|
||||
const quotedMessage = await this.store.loadMessage(
|
||||
toJID(request.chatId),
|
||||
id,
|
||||
);
|
||||
const options = await this.getMessageOptions(request);
|
||||
const message = {
|
||||
text: request.text,
|
||||
mentions: request.mentions?.map(toJID),
|
||||
};
|
||||
return await this.sock.sendMessage(request.chatId, message, {
|
||||
quoted: quotedMessage,
|
||||
});
|
||||
return await this.sock.sendMessage(request.chatId, message, options);
|
||||
}
|
||||
|
||||
sendImage(request: MessageImageRequest) {
|
||||
@@ -637,11 +632,14 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
async sendSeen(request: SendSeenRequest) {
|
||||
const key = parseMessageId(request.messageId);
|
||||
const key = parseMessageIdSerialized(request.messageId);
|
||||
const participant = request.participant
|
||||
? toJID(this.ensureSuffix(request.participant))
|
||||
: undefined;
|
||||
const data = {
|
||||
remoteJid: key.remoteJid,
|
||||
id: key.id,
|
||||
participant: request.participant,
|
||||
participant: participant,
|
||||
};
|
||||
return this.sock.readMessages([data]);
|
||||
}
|
||||
@@ -668,19 +666,18 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
toJID(chatId),
|
||||
toNumber(limit),
|
||||
);
|
||||
const result = [];
|
||||
|
||||
const promises = [];
|
||||
for (const msg of messages) {
|
||||
const wamsg = await this.processIncomingMessage(msg, downloadMedia);
|
||||
if (!wamsg) {
|
||||
continue;
|
||||
}
|
||||
result.push(wamsg);
|
||||
promises.push(this.processIncomingMessage(msg, downloadMedia));
|
||||
}
|
||||
let result = await Promise.all(promises);
|
||||
result = result.filter(Boolean);
|
||||
return result;
|
||||
}
|
||||
|
||||
async setReaction(request: MessageReactionRequest) {
|
||||
const key = parseMessageId(request.messageId);
|
||||
const key = parseMessageIdSerialized(request.messageId);
|
||||
const reactionMessage = {
|
||||
react: {
|
||||
text: request.reaction,
|
||||
@@ -691,7 +688,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
async setStar(request: MessageStarRequest) {
|
||||
const key = parseMessageId(request.messageId);
|
||||
const key = parseMessageIdSerialized(request.messageId);
|
||||
await this.sock.chatModify(
|
||||
{
|
||||
star: {
|
||||
@@ -930,13 +927,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
|
||||
public deleteStatus(request: DeleteStatusRequest) {
|
||||
const messageId = request.id;
|
||||
let key: any;
|
||||
if (messageId.includes('_')) {
|
||||
key = parseMessageId(messageId);
|
||||
} else {
|
||||
key = { id: messageId };
|
||||
}
|
||||
|
||||
const key = parseMessageIdSerialized(messageId, true);
|
||||
key.fromMe = true;
|
||||
key.remoteJid = BROADCAST_ID;
|
||||
const JIDs = request.contacts.map(toJID);
|
||||
@@ -1016,19 +1007,19 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
public async channelsFollowChannel(id: string): Promise<void> {
|
||||
return await this.sock.newsletterFollow(id);
|
||||
return await this.sock.newsletterAction(id, 'follow');
|
||||
}
|
||||
|
||||
public async channelsUnfollowChannel(id: string): Promise<void> {
|
||||
return await this.sock.newsletterUnfollow(id);
|
||||
return await this.sock.newsletterAction(id, 'unfollow');
|
||||
}
|
||||
|
||||
public async channelsMuteChannel(id: string): Promise<void> {
|
||||
return await this.sock.newsletterMute(id);
|
||||
return await this.sock.newsletterAction(id, 'mute');
|
||||
}
|
||||
|
||||
public async channelsUnmuteChannel(id: string): Promise<void> {
|
||||
return await this.sock.newsletterUnmute(id);
|
||||
return await this.sock.newsletterAction(id, 'unmute');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1266,18 +1257,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
protected toWAMessage(message): Promise<WAMessage> {
|
||||
const fromToParticipant = getFromToParticipant(message);
|
||||
const id = buildMessageId(message.key);
|
||||
let body = message.message.conversation;
|
||||
if (!body) {
|
||||
// Some of the messages have no conversation, but instead have text in extendedTextMessage
|
||||
// https://github.com/devlikeapro/waha/issues/90
|
||||
body = message.message.extendedTextMessage?.text;
|
||||
}
|
||||
if (!body) {
|
||||
// Populate from caption
|
||||
const mediaContent = extractMediaContent(message.message);
|
||||
// @ts-ignore - AudioMessage doesn't have caption field
|
||||
body = mediaContent?.caption;
|
||||
}
|
||||
const body = this.extractBody(message.message);
|
||||
const replyTo = this.extractReplyTo(message.message);
|
||||
const ack = message.ack || message.status - 1;
|
||||
return Promise.resolve({
|
||||
id: id,
|
||||
@@ -1297,10 +1278,45 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
ackName: WAMessageAck[ack] || ACK_UNKNOWN,
|
||||
location: message.location,
|
||||
vCards: message.vCards,
|
||||
replyTo: replyTo,
|
||||
_data: message,
|
||||
});
|
||||
}
|
||||
|
||||
protected extractBody(message): string | null {
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
let body = message.conversation;
|
||||
if (!body) {
|
||||
// Some of the messages have no conversation, but instead have text in extendedTextMessage
|
||||
// https://github.com/devlikeapro/waha/issues/90
|
||||
body = message.extendedTextMessage?.text;
|
||||
}
|
||||
if (!body) {
|
||||
// Populate from caption
|
||||
const mediaContent = extractMediaContent(message);
|
||||
// @ts-ignore - AudioMessage doesn't have caption field
|
||||
body = mediaContent?.caption;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
protected extractReplyTo(message): ReplyToMessage | null {
|
||||
const msgType = getContentType(message);
|
||||
const contextInfo = message[msgType]?.contextInfo;
|
||||
if (!contextInfo) {
|
||||
return null;
|
||||
}
|
||||
const quotedMessage = contextInfo.quotedMessage;
|
||||
const body = this.extractBody(quotedMessage);
|
||||
return {
|
||||
id: contextInfo.stanzaId,
|
||||
participant: contextInfo.participant,
|
||||
body: body,
|
||||
};
|
||||
}
|
||||
|
||||
protected toWAContact(contact: Contact) {
|
||||
contact.id = toCusFormat(contact.id);
|
||||
// @ts-ignore
|
||||
@@ -1464,11 +1480,23 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
|
||||
protected downloadMedia(message) {
|
||||
const processor = new EngineMediaProcessor(this);
|
||||
return this.mediaManager.processMedia(processor, message);
|
||||
return this.mediaManager.processMedia(processor, message, this.name);
|
||||
}
|
||||
|
||||
protected async getMessageOptions(request: any): Promise<any> {
|
||||
let quoted;
|
||||
if (request.reply_to) {
|
||||
const key = parseMessageIdSerialized(request.reply_to, true);
|
||||
quoted = await this.store.loadMessage(toJID(request.chatId), key.id);
|
||||
}
|
||||
|
||||
return {
|
||||
quoted: quoted,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class EngineMediaProcessor implements IEngineMediaProcessor<any> {
|
||||
export class EngineMediaProcessor implements IMediaEngineProcessor<any> {
|
||||
constructor(public session: WhatsappSessionNoWebCore) {}
|
||||
|
||||
hasMedia(message: any): boolean {
|
||||
@@ -1555,7 +1583,11 @@ function buildMessageId({ id, remoteJid, fromMe }) {
|
||||
* false_11111111111@c.us_AAA
|
||||
* {id: "AAA", remoteJid: "11111111111@s.whatsapp.net", "fromMe": false}
|
||||
*/
|
||||
function parseMessageId(messageId) {
|
||||
function parseMessageIdSerialized(messageId: string, soft: boolean = false) {
|
||||
if (!messageId.includes('_') && soft) {
|
||||
return { id: messageId };
|
||||
}
|
||||
|
||||
const parts = messageId.split('_');
|
||||
if (parts.length != 3) {
|
||||
throw new Error(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Chat, Contact, makeInMemoryStore, proto } from '@adiwajshing/baileys';
|
||||
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import { BadRequestException, ConsoleLogger } from '@nestjs/common';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { INowebStore } from './INowebStore';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import * as Buffer from 'buffer';
|
||||
import { create, CreateConfig, Message, Whatsapp } from 'venom-bot';
|
||||
|
||||
@@ -21,7 +22,6 @@ import {
|
||||
WAHASessionStatus,
|
||||
} from '../../../structures/enums.dto';
|
||||
import { WAMessage } from '../../../structures/responses.dto';
|
||||
import { IEngineMediaProcessor } from '../../abc/media.abc';
|
||||
import { WAHAInternalEvent, WhatsappSession } from '../../abc/session.abc';
|
||||
import { NotImplementedByEngineError } from '../../exceptions';
|
||||
import { QR } from '../../QR';
|
||||
@@ -248,7 +248,7 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
|
||||
|
||||
protected downloadMedia(message: Message) {
|
||||
const processor = new EngineMediaProcessor(this);
|
||||
return this.mediaManager.processMedia(processor, message);
|
||||
return this.mediaManager.processMedia(processor, message, this.name);
|
||||
}
|
||||
|
||||
private processIncomingMessage(message: Message) {
|
||||
@@ -280,7 +280,7 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
|
||||
}
|
||||
}
|
||||
|
||||
export class EngineMediaProcessor implements IEngineMediaProcessor<Message> {
|
||||
export class EngineMediaProcessor implements IMediaEngineProcessor<Message> {
|
||||
constructor(public session: WhatsappSessionVenomCore) {}
|
||||
|
||||
hasMedia(message: any): boolean {
|
||||
|
||||
@@ -7,8 +7,11 @@ const { LoadUtils } = require('./WahaInjected');
|
||||
const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory');
|
||||
|
||||
export class WebjsClient extends Client {
|
||||
async initialize() {
|
||||
await super.initialize();
|
||||
async inject() {
|
||||
// Even tho this.inject is not defined as interface in Client.ts
|
||||
// We can still call and override it
|
||||
// @ts-ignore
|
||||
await super.inject();
|
||||
//Load util functions (serializers, helper functions)
|
||||
await this.pupPage.evaluate(LoadUtils);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
|
||||
import { IEngineMediaProcessor } from '@waha/core/abc/media.abc';
|
||||
import {
|
||||
getChannelInviteLink,
|
||||
WAHAInternalEvent,
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
AvailableInPlusVersion,
|
||||
NotImplementedByEngineError,
|
||||
} from '@waha/core/exceptions';
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { QR } from '@waha/core/QR';
|
||||
import { parseBool } from '@waha/helpers';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
@@ -52,9 +52,11 @@ import {
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '@waha/structures/groups.dto';
|
||||
import { Label, LabelID } from '@waha/structures/labels.dto';
|
||||
import { ReplyToMessage } from '@waha/structures/message.dto';
|
||||
import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
|
||||
import { MeInfo } from '@waha/structures/sessions.dto';
|
||||
import { WAMessageRevokedBody } from '@waha/structures/webhooks.dto';
|
||||
import { waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner';
|
||||
import {
|
||||
Call,
|
||||
@@ -152,6 +154,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
);
|
||||
return;
|
||||
}
|
||||
await this.end();
|
||||
await this.start();
|
||||
});
|
||||
}
|
||||
@@ -174,7 +177,6 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
await this.end();
|
||||
this.shouldRestart = true;
|
||||
this.whatsapp = await this.buildClient();
|
||||
this.whatsapp
|
||||
@@ -234,8 +236,18 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
try {
|
||||
this.whatsapp?.removeAllListeners();
|
||||
this.startDelayedJob.cancel();
|
||||
// It's possible that browser yet starting
|
||||
await waitUntil(
|
||||
async () => {
|
||||
const result = !!this.whatsapp.pupBrowser;
|
||||
this.logger.debug(`Browser is ready to be closed: ${result}`);
|
||||
return result;
|
||||
},
|
||||
1_000,
|
||||
10_000,
|
||||
);
|
||||
this.whatsapp?.destroy().catch((error) => {
|
||||
this.logger.debug('Failed to destroy the client', error);
|
||||
this.logger.warn(error, 'Failed to destroy the client');
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
@@ -341,10 +353,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
sendText(request: MessageTextRequest) {
|
||||
const options = {
|
||||
// It's fine to sent just ids instead of Contact object
|
||||
mentions: request.mentions as unknown as string[],
|
||||
};
|
||||
const options = this.getMessageOptions(request);
|
||||
return this.whatsapp.sendMessage(
|
||||
this.ensureSuffix(request.chatId),
|
||||
request.text,
|
||||
@@ -371,11 +380,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
reply(request: MessageReplyRequest) {
|
||||
const options = {
|
||||
quotedMessageId: request.reply_to,
|
||||
// It's fine to sent just ids instead of Contact object
|
||||
mentions: request.mentions as unknown as string[],
|
||||
};
|
||||
const options = this.getMessageOptions(request);
|
||||
return this.whatsapp.sendMessage(request.chatId, request.text, options);
|
||||
}
|
||||
|
||||
@@ -395,7 +400,8 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
const location = new Location(request.latitude, request.longitude, {
|
||||
name: request.title,
|
||||
});
|
||||
return this.whatsapp.sendMessage(request.chatId, location);
|
||||
const options = this.getMessageOptions(request);
|
||||
return this.whatsapp.sendMessage(request.chatId, location, options);
|
||||
}
|
||||
|
||||
async sendSeen(request: SendSeenRequest) {
|
||||
@@ -457,12 +463,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
const messages = await chat.fetchMessages({
|
||||
limit: limit,
|
||||
});
|
||||
// Go over messages, download media, and convert to right format.
|
||||
const result = [];
|
||||
for (const message of messages) {
|
||||
const msg = await this.processIncomingMessage(message, downloadMedia);
|
||||
result.push(msg);
|
||||
const promises = [];
|
||||
for (const msg of messages) {
|
||||
promises.push(this.processIncomingMessage(msg, downloadMedia));
|
||||
}
|
||||
let result = await Promise.all(promises);
|
||||
result = result.filter(Boolean);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -920,6 +926,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
protected toWAMessage(message: Message): Promise<WAMessage> {
|
||||
const replyTo = this.extractReplyTo(message);
|
||||
// @ts-ignore
|
||||
return Promise.resolve({
|
||||
id: message.id._serialized,
|
||||
@@ -940,10 +947,24 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
ackName: WAMessageAck[message.ack] || ACK_UNKNOWN,
|
||||
location: message.location,
|
||||
vCards: message.vCards,
|
||||
replyTo: replyTo,
|
||||
_data: message.rawData,
|
||||
});
|
||||
}
|
||||
|
||||
protected extractReplyTo(message: Message): ReplyToMessage | null {
|
||||
// @ts-ignore
|
||||
const quotedMsg = message.rawData.quotedMsg;
|
||||
if (!quotedMsg) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
id: quotedMsg.id?.id,
|
||||
participant: quotedMsg.author || quotedMsg.from,
|
||||
body: quotedMsg.caption || quotedMsg.body,
|
||||
};
|
||||
}
|
||||
|
||||
public async getEngineInfo() {
|
||||
// Add 1 seconds timeout
|
||||
return {
|
||||
@@ -960,11 +981,23 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
|
||||
protected downloadMedia(message: Message) {
|
||||
const processor = new EngineMediaProcessor();
|
||||
return this.mediaManager.processMedia(processor, message);
|
||||
return this.mediaManager.processMedia(processor, message, this.name);
|
||||
}
|
||||
|
||||
protected getMessageOptions(request: any): any {
|
||||
let mentions = request.mentions;
|
||||
mentions = mentions ? mentions.map(this.ensureSuffix) : undefined;
|
||||
|
||||
const quotedMessageId = request.reply_to;
|
||||
|
||||
return {
|
||||
mentions: mentions,
|
||||
quotedMessageId: quotedMessageId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class EngineMediaProcessor implements IEngineMediaProcessor<Message> {
|
||||
export class EngineMediaProcessor implements IMediaEngineProcessor<Message> {
|
||||
hasMedia(message: Message): boolean {
|
||||
if (!message.hasMedia) {
|
||||
return false;
|
||||
|
||||
+93
-67
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
ConsoleLogger,
|
||||
Injectable,
|
||||
LogLevel,
|
||||
NotFoundException,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common';
|
||||
import { MediaNoopStorage } from '@waha/core/media/MediaNoopStorage';
|
||||
import { getPinoLogLevel, LoggerBuilder } from '@waha/utils/logging';
|
||||
import { promiseTimeout } from '@waha/utils/promiseTimeout';
|
||||
import { promiseTimeout, sleep } from '@waha/utils/promiseTimeout';
|
||||
import { EventEmitter } from 'events';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
@@ -18,11 +18,9 @@ import {
|
||||
} from '../structures/enums.dto';
|
||||
import {
|
||||
ProxyConfig,
|
||||
SessionConfig,
|
||||
SessionDTO,
|
||||
SessionInfo,
|
||||
SessionLogoutRequest,
|
||||
SessionStartRequest,
|
||||
SessionStopRequest,
|
||||
} from '../structures/sessions.dto';
|
||||
import { WebhookConfig } from '../structures/webhooks.config.dto';
|
||||
import { SessionManager } from './abc/manager.abc';
|
||||
@@ -33,9 +31,8 @@ import { WhatsappSessionVenomCore } from './engines/venom/session.venom.core';
|
||||
import { WhatsappSessionWebJSCore } from './engines/webjs/session.webjs.core';
|
||||
import { DOCS_URL } from './exceptions';
|
||||
import { getProxyConfig } from './helpers.proxy';
|
||||
import { CoreMediaManager, MediaStorageCore } from './media.core';
|
||||
import { MediaManagerCore } from './media/MediaManagerCore';
|
||||
import { LocalSessionAuthRepository } from './storage/LocalSessionAuthRepository';
|
||||
import { LocalSessionConfigRepository } from './storage/LocalSessionConfigRepository';
|
||||
import { LocalStoreCore } from './storage/LocalStoreCore';
|
||||
import { WebhookConductorCore } from './webhooks.core';
|
||||
|
||||
@@ -49,7 +46,13 @@ export class OnlyDefaultSessionIsAllowed extends UnprocessableEntityException {
|
||||
|
||||
@Injectable()
|
||||
export class SessionManagerCore extends SessionManager {
|
||||
private session?: WhatsappSession;
|
||||
SESSION_STOP_TIMEOUT = 3000;
|
||||
|
||||
// session - exists and running (or failed or smth)
|
||||
// null - stopped
|
||||
// undefined - removed
|
||||
private session: WhatsappSession | undefined | null;
|
||||
private sessionConfig?: SessionConfig;
|
||||
DEFAULT = 'default';
|
||||
|
||||
// @ts-ignore
|
||||
@@ -63,20 +66,20 @@ export class SessionManagerCore extends SessionManager {
|
||||
) {
|
||||
super();
|
||||
this.events = new EventEmitter();
|
||||
this.session = null;
|
||||
this.sessionConfig = null;
|
||||
this.log.setContext(SessionManagerCore.name);
|
||||
this.session = undefined;
|
||||
const engineName = this.engineConfigService.getDefaultEngineName();
|
||||
this.EngineClass = this.getEngine(engineName);
|
||||
this.store = new LocalStoreCore(engineName.toLowerCase());
|
||||
this.sessionAuthRepository = new LocalSessionAuthRepository(this.store);
|
||||
this.sessionConfigRepository = new LocalSessionConfigRepository(this.store);
|
||||
this.startPredefinedSessions();
|
||||
}
|
||||
|
||||
protected startPredefinedSessions() {
|
||||
const startSessions = this.config.startSessions;
|
||||
startSessions.forEach((sessionName) => {
|
||||
this.start({ name: sessionName });
|
||||
this.start(sessionName);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,32 +105,48 @@ export class SessionManagerCore extends SessionManager {
|
||||
if (!this.session) {
|
||||
return;
|
||||
}
|
||||
await this.stop({ name: this.DEFAULT, logout: false });
|
||||
await this.stop(this.DEFAULT, true);
|
||||
}
|
||||
|
||||
//
|
||||
// API Methods
|
||||
//
|
||||
async start(request: SessionStartRequest): Promise<SessionDTO> {
|
||||
this.onlyDefault(request.name);
|
||||
async exists(name: string): Promise<boolean> {
|
||||
this.onlyDefault(name);
|
||||
return this.session !== undefined;
|
||||
}
|
||||
|
||||
isRunning(name: string): boolean {
|
||||
this.onlyDefault(name);
|
||||
return !!this.session;
|
||||
}
|
||||
|
||||
async upsert(name: string, config?: SessionConfig): Promise<void> {
|
||||
this.onlyDefault(name);
|
||||
this.sessionConfig = config;
|
||||
}
|
||||
|
||||
async start(name: string): Promise<SessionDTO> {
|
||||
this.onlyDefault(name);
|
||||
if (this.session) {
|
||||
throw new UnprocessableEntityException(
|
||||
`Session '${this.DEFAULT}' is already started.`,
|
||||
);
|
||||
}
|
||||
|
||||
const name = request.name;
|
||||
this.log.info(`'${name}' - starting session...`);
|
||||
const mediaManager = new CoreMediaManager(
|
||||
new MediaStorageCore(),
|
||||
const mediaManager = new MediaManagerCore(
|
||||
new MediaNoopStorage(),
|
||||
this.config.mimetypes,
|
||||
);
|
||||
const logger = this.log.logger.child({ session: name });
|
||||
logger.level = getPinoLogLevel(request.config?.debug);
|
||||
const logger = this.log.logger.child({
|
||||
session: name,
|
||||
sessionRunTimestamp: Date.now(),
|
||||
});
|
||||
logger.level = getPinoLogLevel(this.sessionConfig?.debug);
|
||||
const loggerBuilder: LoggerBuilder = logger;
|
||||
|
||||
const webhook = new this.WebhookConductorClass(loggerBuilder);
|
||||
const proxyConfig = this.getProxyConfig(request);
|
||||
const proxyConfig = this.getProxyConfig();
|
||||
const sessionConfig: SessionParams = {
|
||||
name,
|
||||
mediaManager,
|
||||
@@ -135,7 +154,7 @@ export class SessionManagerCore extends SessionManager {
|
||||
printQR: this.engineConfigService.shouldPrintQR,
|
||||
sessionStore: this.store,
|
||||
proxyConfig: proxyConfig,
|
||||
sessionConfig: request.config,
|
||||
sessionConfig: this.sessionConfig,
|
||||
};
|
||||
await this.sessionAuthRepository.init(name);
|
||||
// @ts-ignore
|
||||
@@ -143,7 +162,7 @@ export class SessionManagerCore extends SessionManager {
|
||||
this.session = session;
|
||||
|
||||
// configure webhooks
|
||||
const webhooks = this.getWebhooks(request);
|
||||
const webhooks = this.getWebhooks();
|
||||
webhook.configure(session, webhooks);
|
||||
|
||||
// configure events
|
||||
@@ -154,6 +173,7 @@ export class SessionManagerCore extends SessionManager {
|
||||
|
||||
// start session
|
||||
await session.start();
|
||||
logger.info('Session has been started.');
|
||||
return {
|
||||
name: session.name,
|
||||
status: session.status,
|
||||
@@ -161,13 +181,46 @@ export class SessionManagerCore extends SessionManager {
|
||||
};
|
||||
}
|
||||
|
||||
async stop(name: string, silent: boolean): Promise<void> {
|
||||
this.onlyDefault(name);
|
||||
if (!this.isRunning(name)) {
|
||||
this.log.debug(`Session is not running.`, { session: name });
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.info(`Stopping session...`, { session: name });
|
||||
try {
|
||||
const session = this.getSession(name);
|
||||
await session.stop();
|
||||
} catch (err) {
|
||||
this.log.warn(`Error while stopping session '${name}'`);
|
||||
if (!silent) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
this.log.info(`Session has been stopped.`, { session: name });
|
||||
this.session = null;
|
||||
await sleep(this.SESSION_STOP_TIMEOUT);
|
||||
}
|
||||
|
||||
async logout(name: string): Promise<void> {
|
||||
this.onlyDefault(name);
|
||||
await this.sessionAuthRepository.clean(name);
|
||||
}
|
||||
|
||||
async delete(name: string): Promise<void> {
|
||||
this.onlyDefault(name);
|
||||
this.session = undefined;
|
||||
this.sessionConfig = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine per session and global webhooks
|
||||
*/
|
||||
private getWebhooks(request: SessionStartRequest) {
|
||||
private getWebhooks() {
|
||||
let webhooks: WebhookConfig[] = [];
|
||||
if (request.config?.webhooks) {
|
||||
webhooks = webhooks.concat(request.config.webhooks);
|
||||
if (this.sessionConfig?.webhooks) {
|
||||
webhooks = webhooks.concat(this.sessionConfig.webhooks);
|
||||
}
|
||||
const globalWebhookConfig = this.config.getWebhookConfig();
|
||||
if (globalWebhookConfig) {
|
||||
@@ -179,71 +232,44 @@ export class SessionManagerCore extends SessionManager {
|
||||
/**
|
||||
* Get either session's or global proxy if defined
|
||||
*/
|
||||
protected getProxyConfig(
|
||||
request: SessionStartRequest,
|
||||
): ProxyConfig | undefined {
|
||||
if (request.config?.proxy) {
|
||||
return request.config.proxy;
|
||||
protected getProxyConfig(): ProxyConfig | undefined {
|
||||
if (this.sessionConfig?.proxy) {
|
||||
return this.sessionConfig.proxy;
|
||||
}
|
||||
if (!this.session) {
|
||||
return undefined;
|
||||
}
|
||||
const sessions = { [request.name]: this.session };
|
||||
return getProxyConfig(this.config, sessions, request.name);
|
||||
}
|
||||
|
||||
async stop(request: SessionStopRequest): Promise<void> {
|
||||
this.onlyDefault(request.name);
|
||||
|
||||
const name = request.name;
|
||||
this.log.info(`Stopping ${name} session...`);
|
||||
const session = this.getSession(name);
|
||||
await session.stop();
|
||||
this.log.info(`"${name}" has been stopped.`);
|
||||
this.session = undefined;
|
||||
}
|
||||
|
||||
async logout(request: SessionLogoutRequest) {
|
||||
const name = request.name;
|
||||
this.onlyDefault(request.name);
|
||||
this.stop({ name: name, logout: false })
|
||||
.then(() => {
|
||||
this.log.info(`Session '${name}' has been stopped.`);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.log.error(
|
||||
`Error while stopping session '${name}' while logging out`,
|
||||
err,
|
||||
);
|
||||
});
|
||||
await this.sessionAuthRepository.clean(request.name);
|
||||
const sessions = { [this.DEFAULT]: this.session };
|
||||
return getProxyConfig(this.config, sessions, this.DEFAULT);
|
||||
}
|
||||
|
||||
getSession(name: string): WhatsappSession {
|
||||
this.onlyDefault(name);
|
||||
const session = this.session;
|
||||
if (!session) {
|
||||
if (session === undefined) {
|
||||
throw new NotFoundException(
|
||||
`We didn't find a session with name '${name}'. Please start it first by using POST /sessions/start request`,
|
||||
`We didn't find a session with name '${name}'.
|
||||
Please start it first by using POST /sessions/${name}/start request`,
|
||||
);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
async getSessions(all: boolean): Promise<SessionInfo[]> {
|
||||
if (!this.session) {
|
||||
if (!all) {
|
||||
return [];
|
||||
}
|
||||
if (this.session === null && all) {
|
||||
return [
|
||||
{
|
||||
name: this.DEFAULT,
|
||||
status: WAHASessionStatus.STOPPED,
|
||||
config: undefined,
|
||||
config: this.sessionConfig,
|
||||
me: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (this.session === undefined && all) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const me = this.session.getSessionMeInfo();
|
||||
// Get engine info
|
||||
let engineInfo = {};
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { WAMedia } from '../structures/responses.dto';
|
||||
import {
|
||||
IEngineMediaProcessor,
|
||||
MediaManager,
|
||||
MediaStorage,
|
||||
} from './abc/media.abc';
|
||||
import { DOCS_URL } from './exceptions';
|
||||
|
||||
export class MediaStorageCore implements MediaStorage {
|
||||
async save(
|
||||
messageId: string,
|
||||
mimetype: string,
|
||||
buffer: Buffer,
|
||||
): Promise<string> {
|
||||
return Promise.resolve(
|
||||
`Media attachment's available only in WAHA Plus version. ${DOCS_URL}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class CoreMediaManager implements MediaManager {
|
||||
constructor(
|
||||
private storage: MediaStorage,
|
||||
private mimetypes: string[],
|
||||
) {}
|
||||
|
||||
async processMedia<Message>(
|
||||
processor: IEngineMediaProcessor<Message>,
|
||||
message: Message,
|
||||
): Promise<Message> {
|
||||
if (!processor.hasMedia(message)) {
|
||||
return message;
|
||||
}
|
||||
const mimetype = '';
|
||||
const filename = processor.getFilename(message);
|
||||
const url = await this.storage.save('', mimetype, Buffer.from(''));
|
||||
const media: WAMedia = {
|
||||
mimetype: mimetype,
|
||||
filename: filename,
|
||||
url: url,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
message.media = media;
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Engine specific media processor
|
||||
* Knows how to extract necessary attributes and fetch the data from Message
|
||||
*/
|
||||
interface IMediaEngineProcessor<Message> {
|
||||
hasMedia(message: Message): boolean;
|
||||
|
||||
getFilename(message: Message): string | null;
|
||||
|
||||
getMimetype(message: Message): string;
|
||||
|
||||
getMessageId(message: Message): string;
|
||||
|
||||
getMediaBuffer(message: Message): Promise<Buffer | null>;
|
||||
}
|
||||
|
||||
export { IMediaEngineProcessor };
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
|
||||
/**
|
||||
* General interface for MediaManager - one that handles the logic
|
||||
* and manipulates MediaStorage and MediaEngineProcessor
|
||||
*/
|
||||
interface IMediaManager {
|
||||
processMedia<Message>(
|
||||
processor: IMediaEngineProcessor<Message>,
|
||||
message: Message,
|
||||
session: string,
|
||||
): Promise<Message>;
|
||||
}
|
||||
|
||||
export { IMediaManager };
|
||||
@@ -0,0 +1,42 @@
|
||||
import { S3MediaData } from '@waha/structures/media.s3.dto';
|
||||
|
||||
/**
|
||||
* Handles saving data to the physical storage
|
||||
*/
|
||||
interface Message {
|
||||
// false_111111111@c.us_AAAAAAAAAAAAAA
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface File {
|
||||
extension: string;
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
export interface MediaData {
|
||||
session: string;
|
||||
message: Message;
|
||||
file: File;
|
||||
}
|
||||
|
||||
/**
|
||||
* For stored media data
|
||||
*/
|
||||
export interface MediaStorageData {
|
||||
url: string;
|
||||
s3?: S3MediaData;
|
||||
}
|
||||
|
||||
abstract class IMediaStorage {
|
||||
abstract init(): Promise<void>;
|
||||
|
||||
abstract save(buffer: Buffer, data: MediaData): Promise<boolean>;
|
||||
|
||||
abstract exists(data: MediaData): Promise<boolean>;
|
||||
|
||||
abstract getStorageData(data: MediaData): Promise<MediaStorageData>;
|
||||
|
||||
abstract purge(): Promise<void>;
|
||||
}
|
||||
|
||||
export { IMediaStorage };
|
||||
@@ -0,0 +1,44 @@
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { IMediaStorage, MediaData } from '@waha/core/media/IMediaStorage';
|
||||
import { WAMedia } from '@waha/structures/media.dto';
|
||||
|
||||
import { IMediaManager } from './IMediaManager';
|
||||
|
||||
export class MediaManagerCore implements IMediaManager {
|
||||
constructor(
|
||||
private storage: IMediaStorage,
|
||||
private mimetypes: string[],
|
||||
) {}
|
||||
|
||||
async processMedia<Message>(
|
||||
processor: IMediaEngineProcessor<Message>,
|
||||
message: Message,
|
||||
session: string,
|
||||
): Promise<Message> {
|
||||
if (!processor.hasMedia(message)) {
|
||||
return message;
|
||||
}
|
||||
const mimetype = '';
|
||||
const mediaData: MediaData = {
|
||||
session: session,
|
||||
message: {
|
||||
id: '',
|
||||
},
|
||||
file: {
|
||||
extension: '',
|
||||
},
|
||||
};
|
||||
const { url } = await this.storage.getStorageData(mediaData);
|
||||
|
||||
const filename = processor.getFilename(message);
|
||||
const media: WAMedia = {
|
||||
mimetype: mimetype,
|
||||
filename: filename,
|
||||
url: url,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
message.media = media;
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { DOCS_URL } from '@waha/core/exceptions';
|
||||
import { IMediaStorage, MediaData } from '@waha/core/media/IMediaStorage';
|
||||
|
||||
export class MediaNoopStorage implements IMediaStorage {
|
||||
async init() {
|
||||
return;
|
||||
}
|
||||
|
||||
async save(buffer: Buffer, data: MediaData): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
async exists(data: MediaData): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async getStorageData(data: MediaData) {
|
||||
const url = `Media attachment's available only in WAHA Plus version. ${DOCS_URL}`;
|
||||
return { url };
|
||||
}
|
||||
|
||||
async purge() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { WhatsappConfigService } from '@waha/config.service';
|
||||
|
||||
@Injectable()
|
||||
export class MediaLocalStorageConfig {
|
||||
public filesUri = '/api/files';
|
||||
|
||||
constructor(private config: WhatsappConfigService) {}
|
||||
|
||||
get filesURL(): string {
|
||||
return `${this.config.schema}://${this.config.hostname}:${this.config.port}${this.filesUri}/`;
|
||||
}
|
||||
|
||||
get filesFolder(): string {
|
||||
return this.config.get('WHATSAPP_FILES_FOLDER', '/tmp/whatsapp-files');
|
||||
}
|
||||
|
||||
get filesLifetime(): number {
|
||||
return parseInt(this.config.get('WHATSAPP_FILES_LIFETIME', '180'));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,4 @@ export abstract class ISessionAuthRepository {
|
||||
abstract init(sessionName?: string): Promise<void>;
|
||||
|
||||
abstract clean(sessionName: string): Promise<void>;
|
||||
|
||||
abstract getAll(): Promise<string[]>;
|
||||
}
|
||||
@@ -8,5 +8,9 @@ export abstract class ISessionConfigRepository {
|
||||
|
||||
abstract get(sessionName: string): Promise<SessionConfig | null>;
|
||||
|
||||
abstract exists(sessionName: string): Promise<boolean>;
|
||||
|
||||
abstract delete(sessionName: string): Promise<void>;
|
||||
|
||||
abstract getAll(): Promise<string[]>;
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import * as fs from 'fs/promises';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const fs = require('fs-extra');
|
||||
|
||||
import { ISessionAuthRepository } from './ISessionAuthRepository';
|
||||
import { LocalStore } from './LocalStore';
|
||||
// Keep all waha related files, ".waha.session.*"
|
||||
const KEEP_FILES = /^\.waha\.session\..*$/;
|
||||
|
||||
export class LocalSessionAuthRepository extends ISessionAuthRepository {
|
||||
private store: LocalStore;
|
||||
@@ -16,17 +19,17 @@ export class LocalSessionAuthRepository extends ISessionAuthRepository {
|
||||
}
|
||||
|
||||
async clean(sessionName: string) {
|
||||
const folder = this.store.getSessionDirectory(sessionName);
|
||||
await fs.rm(folder, { recursive: true });
|
||||
}
|
||||
|
||||
async getAll(): Promise<string[]> {
|
||||
await this.init();
|
||||
const content = await fs.readdir(this.store.getEngineDirectory(), {
|
||||
withFileTypes: true,
|
||||
});
|
||||
return content
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
// Remove all files and directories recursively, but keep waha files
|
||||
const sessionDirectory = this.store.getSessionDirectory(sessionName);
|
||||
// Check it exists and it's directory
|
||||
const exists = await fs.pathExists(sessionDirectory);
|
||||
if (!exists) {
|
||||
return;
|
||||
}
|
||||
const files = await fs.readdir(sessionDirectory);
|
||||
const filesToRemove = files.filter((file) => !file.match(KEEP_FILES));
|
||||
for (const file of filesToRemove) {
|
||||
await fs.remove(`${sessionDirectory}/${file}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const fs = require('fs-extra');
|
||||
|
||||
import { fileExists } from '@waha/utils/files';
|
||||
|
||||
import { SessionConfig } from '../../structures/sessions.dto';
|
||||
import { ISessionConfigRepository } from './ISessionConfigRepository';
|
||||
@@ -14,19 +16,15 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
private async fileExists(filepath: string) {
|
||||
try {
|
||||
await fs.access(filepath, fs.constants.F_OK);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
async exists(sessionName: string): Promise<boolean> {
|
||||
const filepath = this.getFilePath(sessionName);
|
||||
return await fileExists(filepath);
|
||||
}
|
||||
|
||||
async get(sessionName: string): Promise<SessionConfig | null> {
|
||||
const filepath = this.getFilePath(sessionName);
|
||||
// Check file exists
|
||||
if (!(await this.fileExists(filepath))) {
|
||||
if (!(await fileExists(filepath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -56,10 +54,17 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
|
||||
}
|
||||
|
||||
async delete(sessionName: string): Promise<void> {
|
||||
const filepath = this.getFilePath(sessionName);
|
||||
if (!(await this.fileExists(filepath))) {
|
||||
return;
|
||||
}
|
||||
await fs.unlink(filepath);
|
||||
const sessionDirectory = this.store.getSessionDirectory(sessionName);
|
||||
await fs.remove(sessionDirectory);
|
||||
}
|
||||
|
||||
async getAll(): Promise<string[]> {
|
||||
await this.store.init();
|
||||
const content = await fs.readdir(this.store.getEngineDirectory(), {
|
||||
withFileTypes: true,
|
||||
});
|
||||
return content
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ConsoleLogger } from '@nestjs/common';
|
||||
|
||||
import { WAHAEvents } from '../structures/enums.dto';
|
||||
import { WebhookConfig } from '../structures/webhooks.config.dto';
|
||||
import { WAHAWebhook } from '../structures/webhooks.dto';
|
||||
@@ -137,6 +135,7 @@ export class WebhookConductorCore implements WebhookConductor {
|
||||
const json: WAHAWebhook = {
|
||||
event: event,
|
||||
session: session.name,
|
||||
metadata: session.sessionConfig?.metadata,
|
||||
me: me,
|
||||
payload: data,
|
||||
engine: session.engine,
|
||||
|
||||
@@ -13,11 +13,6 @@ function parseBool(value: any): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.error(
|
||||
'parseBool got an unexpected value:',
|
||||
value,
|
||||
'(accepted values : "true", "false")',
|
||||
);
|
||||
throw new Error(
|
||||
'Error: parseBool got unexpected value - use "true" or "false" values',
|
||||
);
|
||||
|
||||
+15
-5
@@ -11,10 +11,10 @@ import { LoggerErrorInterceptor } from 'nestjs-pino';
|
||||
import { Logger } from 'pino';
|
||||
import pino from 'pino';
|
||||
|
||||
import { AllExceptionsFilter } from './api/exception.filter';
|
||||
import { WhatsappConfigService } from './config.service';
|
||||
import { AppModuleCore } from './core/app.module.core';
|
||||
import { SwaggerConfiguratorCore } from './core/SwaggerConfiguratorCore';
|
||||
import { AllExceptionsFilter } from './nestjs/AllExceptionsFilter';
|
||||
import { WAHA_WEBHOOKS } from './structures/webhooks.dto';
|
||||
import { getWAHAVersion, VERSION, WAHAVersion } from './version';
|
||||
|
||||
@@ -23,7 +23,7 @@ const logger: Logger = pino({
|
||||
transport: getPinoTransport(),
|
||||
}).child({ name: 'Bootstrap' });
|
||||
|
||||
logger.info('NODE - Catching unhandled exceptions enabled');
|
||||
logger.info('NODE - Catching unhandled rejection enabled');
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
// @ts-ignore
|
||||
@@ -34,7 +34,6 @@ async function loadModules(): Promise<
|
||||
[typeof AppModuleCore, typeof SwaggerConfiguratorCore]
|
||||
> {
|
||||
const version = getWAHAVersion();
|
||||
logger.info(`WAHA (WhatsApp HTTP API) - Running ${version} version...`);
|
||||
|
||||
if (version === WAHAVersion.CORE) {
|
||||
const { AppModuleCore } = await import('./core/app.module.core');
|
||||
@@ -52,10 +51,18 @@ async function loadModules(): Promise<
|
||||
return [AppModulePlus, SwaggerConfiguratorPlus];
|
||||
}
|
||||
|
||||
let app = undefined;
|
||||
|
||||
export function getApp() {
|
||||
return app;
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const version = getWAHAVersion();
|
||||
logger.info(`WAHA (WhatsApp HTTP API) - Running ${version} version...`);
|
||||
const [AppModule, SwaggerModule] = await loadModules();
|
||||
const httpsOptions = AppModule.getHttpsOptions(logger);
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
app = await NestFactory.create(AppModule, {
|
||||
logger: getNestJSLogLevels(),
|
||||
httpsOptions: httpsOptions,
|
||||
bufferLogs: true,
|
||||
@@ -89,4 +96,7 @@ async function bootstrap() {
|
||||
logger.info(VERSION, 'Environment');
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
bootstrap().catch((error) => {
|
||||
logger.error(error, `Failed to start WAHA: ${error}`);
|
||||
process.exit(1);
|
||||
});
|
||||
File renamed without changes.
@@ -0,0 +1,37 @@
|
||||
import { applyDecorators } from '@nestjs/common';
|
||||
import { ApiExtraModels, ApiResponse, getSchemaPath } from '@nestjs/swagger';
|
||||
import { Base64File } from '@waha/structures/files.dto';
|
||||
|
||||
function getRefSchemaPaths(models) {
|
||||
return models.map((model) => {
|
||||
return { $ref: getSchemaPath(model) };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator to add a file accept header to the swagger documentation
|
||||
*/
|
||||
export function ApiFileAcceptHeader(...models) {
|
||||
models = models.length ? models : [Base64File];
|
||||
return applyDecorators(
|
||||
// Add extra models, otherwise it'll give a error
|
||||
// $ref not found
|
||||
ApiExtraModels(...models),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
content: {
|
||||
'image/png': {
|
||||
schema: {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
},
|
||||
},
|
||||
'application/json': {
|
||||
schema: {
|
||||
oneOf: getRefSchemaPaths(models),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,9 @@
|
||||
import { ApiParam } from '@nestjs/swagger';
|
||||
|
||||
export const ChatIdApiParam = ApiParam({
|
||||
name: 'chatId',
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'Chat ID',
|
||||
example: '123456789@c.us',
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ApiParam } from '@nestjs/swagger';
|
||||
|
||||
export const MessageIdApiParam = ApiParam({
|
||||
name: 'messageId',
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'Message ID',
|
||||
example: 'true_123456789@c.us_BAE6A33293978B16',
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Param } from '@nestjs/common';
|
||||
import { ApiParam } from '@nestjs/swagger';
|
||||
import { SessionPipe } from '@waha/nestjs/pipes/SessionPipe';
|
||||
|
||||
/**
|
||||
* Decorator for a method that uses SessionPipe above
|
||||
*/
|
||||
export const SessionApiParam = ApiParam({
|
||||
name: 'session',
|
||||
required: true,
|
||||
type: 'string',
|
||||
schema: {
|
||||
default: 'default',
|
||||
},
|
||||
description: 'Session <code>name</code>',
|
||||
});
|
||||
/**
|
||||
* Session param
|
||||
*/
|
||||
export const SessionParam = Param('session', SessionPipe);
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Injectable, PipeTransform } from '@nestjs/common';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
|
||||
/**
|
||||
* Get session name and return Whatsapp session back (if exists)
|
||||
* use it as
|
||||
@Param('session', SessionPipe) session: WhatsappSession,
|
||||
*/
|
||||
@Injectable()
|
||||
export class SessionPipe implements PipeTransform<WhatsappSession> {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
async transform(value: any) {
|
||||
return this.manager.getSession(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ValidationPipe, ValidationPipeOptions } from '@nestjs/common';
|
||||
import { parseBool } from '@waha/helpers';
|
||||
|
||||
// So we can change it to True during development and testing
|
||||
const WAHA_HTTP_STRICT_MODE = parseBool(process.env.WAHA_HTTP_STRICT_MODE);
|
||||
|
||||
export class WAHAValidationPipe extends ValidationPipe {
|
||||
constructor(options?: ValidationPipeOptions) {
|
||||
options = options || {};
|
||||
options.transform = true;
|
||||
options.whitelist = true;
|
||||
options.forbidNonWhitelisted = WAHA_HTTP_STRICT_MODE;
|
||||
super(options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Convert string to boolean
|
||||
* @param value
|
||||
* @constructor
|
||||
*/
|
||||
export function BooleanString({ value }: { value: any }) {
|
||||
switch (value) {
|
||||
case 'true':
|
||||
return true;
|
||||
case 'True':
|
||||
return true;
|
||||
case '1':
|
||||
return true;
|
||||
case 'false':
|
||||
return false;
|
||||
case 'False':
|
||||
return false;
|
||||
case '0':
|
||||
return false;
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationArguments,
|
||||
ValidationOptions,
|
||||
} from 'class-validator';
|
||||
|
||||
export function IsDynamicObject(validationOptions?: ValidationOptions) {
|
||||
return function (object: Record<string, any>, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'IsDynamicObject',
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
constraints: [],
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: any, args: ValidationArguments) {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const key in value) {
|
||||
if (typeof key !== 'string' || typeof value[key] !== 'string') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
defaultMessage(validationArguments?: ValidationArguments): string {
|
||||
return `${validationArguments.property} accepts string key-value pairs only`;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LoggerService } from '@nestjs/common';
|
||||
import { WebSocket } from '@waha/utils/ws';
|
||||
import { WebSocket } from '@waha/nestjs/ws/ws';
|
||||
import { WebSocketServer } from 'ws';
|
||||
|
||||
export class WebsocketHeartbeatJob {
|
||||
File renamed without changes.
@@ -15,7 +15,7 @@ import {
|
||||
VoiceBinaryFile,
|
||||
VoiceRemoteFile,
|
||||
} from './files.dto';
|
||||
import { ChatIdProperty } from './properties.dto';
|
||||
import { ChatIdProperty, ReplyToProperty } from './properties.dto';
|
||||
|
||||
/**
|
||||
* Queries
|
||||
@@ -72,6 +72,7 @@ export class SendSeenRequest extends ChatRequest {
|
||||
@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)',
|
||||
})
|
||||
@@ -144,6 +145,9 @@ export class MessageTextRequest extends ChatRequest {
|
||||
text = 'Hi there!';
|
||||
@ApiHideProperty()
|
||||
mentions?: string[];
|
||||
|
||||
@ReplyToProperty()
|
||||
reply_to?: string;
|
||||
}
|
||||
|
||||
export class EditMessageRequest {
|
||||
@@ -155,16 +159,26 @@ export class EditMessageRequest {
|
||||
|
||||
export class MessageReplyRequest extends MessageTextRequest {
|
||||
text = 'Reply text';
|
||||
@ApiProperty({
|
||||
example: 'false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
|
||||
})
|
||||
reply_to: string;
|
||||
}
|
||||
|
||||
export class MessageLocationRequest extends ChatRequest {
|
||||
@ApiProperty({
|
||||
example: 38.8937255,
|
||||
})
|
||||
latitude: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: -77.0969763,
|
||||
})
|
||||
longitude: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Our office',
|
||||
})
|
||||
title: string;
|
||||
|
||||
@ReplyToProperty()
|
||||
reply_to?: string;
|
||||
}
|
||||
|
||||
@ApiExtraModels(BinaryFile, RemoteFile)
|
||||
@@ -180,10 +194,16 @@ class FileRequest extends ChatRequest {
|
||||
|
||||
export class MessageImageRequest extends FileRequest {
|
||||
caption: string;
|
||||
|
||||
@ReplyToProperty()
|
||||
reply_to?: string;
|
||||
}
|
||||
|
||||
export class MessageFileRequest extends FileRequest {
|
||||
caption: string;
|
||||
|
||||
@ReplyToProperty()
|
||||
reply_to?: string;
|
||||
}
|
||||
|
||||
@ApiExtraModels(VoiceBinaryFile, VoiceRemoteFile)
|
||||
@@ -195,6 +215,9 @@ export class MessageVoiceRequest extends ChatRequest {
|
||||
],
|
||||
})
|
||||
file: VoiceBinaryFile | VoiceRemoteFile;
|
||||
|
||||
@ReplyToProperty()
|
||||
reply_to?: string;
|
||||
}
|
||||
|
||||
@ApiExtraModels(VideoRemoteFile, VideoBinaryFile)
|
||||
@@ -208,6 +231,13 @@ export class MessageVideoRequest extends ChatRequest {
|
||||
file: VideoRemoteFile | VideoBinaryFile;
|
||||
|
||||
caption: string = 'Just watch at this!';
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'The ID of the message to reply to - false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
|
||||
example: null,
|
||||
})
|
||||
reply_to?: string;
|
||||
}
|
||||
|
||||
export class MessageLinkPreviewRequest extends ChatRequest {
|
||||
@@ -256,6 +286,13 @@ export class MessagePoll {
|
||||
|
||||
export class MessagePollRequest extends ChatRequest {
|
||||
poll: MessagePoll;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'The ID of the message to reply to - false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
|
||||
example: null,
|
||||
})
|
||||
reply_to?: string;
|
||||
}
|
||||
|
||||
export class MessageDestination {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { S3MediaData } from '@waha/structures/media.s3.dto';
|
||||
|
||||
export class WAMedia {
|
||||
@ApiProperty({
|
||||
description: 'The URL for the media in the message if any',
|
||||
example:
|
||||
'http://localhost:3000/api/files/false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA.oga',
|
||||
})
|
||||
url?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'mimetype for the media in the message if any',
|
||||
example: 'audio/jpeg',
|
||||
})
|
||||
mimetype?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The original filename in mediaUrl in the message if any',
|
||||
example: 'example.pdf',
|
||||
})
|
||||
filename?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'S3 attributes for the media in the message ' +
|
||||
'if you are using S3 media storage',
|
||||
})
|
||||
s3?: S3MediaData;
|
||||
|
||||
@ApiProperty({
|
||||
description: "Error message if there's an error downloading the media",
|
||||
example: null,
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
error?: object;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class S3MediaData {
|
||||
@ApiProperty({
|
||||
description: 'The name of the S3 bucket',
|
||||
example: 'my-bucket',
|
||||
})
|
||||
Bucket: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The key of the object in the S3 bucket',
|
||||
example: 'default/false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA.oga',
|
||||
})
|
||||
Key: string;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
ChatIdProperty,
|
||||
MessageIdOnlyProperty,
|
||||
} from '@waha/structures/properties.dto';
|
||||
|
||||
export class ReplyToMessage {
|
||||
@MessageIdOnlyProperty()
|
||||
id: string;
|
||||
|
||||
@ChatIdProperty()
|
||||
participant?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Hello!',
|
||||
})
|
||||
body?: string;
|
||||
}
|
||||
@@ -15,4 +15,24 @@ function MessageIdProperty() {
|
||||
});
|
||||
}
|
||||
|
||||
export { ChatIdProperty, MessageIdProperty };
|
||||
function MessageIdOnlyProperty() {
|
||||
return ApiProperty({
|
||||
description: 'Message ID',
|
||||
example: 'AAAAAAAAAAAAAAAAAAAA',
|
||||
});
|
||||
}
|
||||
|
||||
function ReplyToProperty() {
|
||||
return ApiProperty({
|
||||
description:
|
||||
'The ID of the message to reply to - false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
|
||||
example: null,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
ChatIdProperty,
|
||||
MessageIdOnlyProperty,
|
||||
MessageIdProperty,
|
||||
ReplyToProperty,
|
||||
};
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { WAMedia } from '@waha/structures/media.dto';
|
||||
import { ReplyToMessage } from '@waha/structures/message.dto';
|
||||
|
||||
import { WAMessageAck } from './enums.dto';
|
||||
import { ChatIdProperty, MessageIdProperty } from './properties.dto';
|
||||
@@ -9,27 +11,6 @@ export class WALocation {
|
||||
longitude: string;
|
||||
}
|
||||
|
||||
export class WAMedia {
|
||||
@ApiProperty({
|
||||
description: 'The URL for the media in the message if any',
|
||||
example:
|
||||
'http://localhost:3000/api/files/false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA.oga',
|
||||
})
|
||||
url: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'mimetype for the media in the message if any',
|
||||
example: 'audio/jpeg',
|
||||
})
|
||||
mimetype?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The original filename in mediaUrl in the message if any',
|
||||
example: 'example.pdf',
|
||||
})
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
class WAMessageBase {
|
||||
@MessageIdProperty()
|
||||
id: string;
|
||||
@@ -78,7 +59,10 @@ export class WAMessage extends WAMessageBase {
|
||||
})
|
||||
hasMedia: boolean;
|
||||
|
||||
media?: WAMedia = null;
|
||||
@ApiProperty({
|
||||
description: 'Media object for the message if any and downloaded',
|
||||
})
|
||||
media?: WAMedia;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
@@ -116,6 +100,8 @@ export class WAMessage extends WAMessageBase {
|
||||
})
|
||||
vCards?: string[];
|
||||
|
||||
replyTo?: ReplyToMessage;
|
||||
|
||||
/** Returns message in a raw format */
|
||||
@ApiProperty({
|
||||
description:
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { BooleanString } from '@waha/nestjs/validation/BooleanString';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsBoolean, IsOptional } from 'class-validator';
|
||||
|
||||
export class EnvironmentQuery {
|
||||
@ApiProperty({
|
||||
example: false,
|
||||
required: false,
|
||||
description: 'Include all environment variables',
|
||||
})
|
||||
@Transform(BooleanString)
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
all: boolean = false;
|
||||
}
|
||||
|
||||
export class StopRequest {
|
||||
@ApiProperty({
|
||||
example: false,
|
||||
required: false,
|
||||
description:
|
||||
'By default, it gracefully stops the server, ' +
|
||||
'but you can force it to terminate immediately.',
|
||||
})
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
force: boolean = false;
|
||||
}
|
||||
|
||||
export class ServerStatusResponse {
|
||||
@ApiProperty({
|
||||
example: 1723788847247,
|
||||
description: 'The timestamp when the server started (milliseconds).',
|
||||
})
|
||||
startTimestamp: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 3600000,
|
||||
description: 'The uptime of the server in milliseconds.',
|
||||
})
|
||||
uptime: number;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { SessionConfig } from '@waha/structures/sessions.dto';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
export class SessionStartDeprecatedRequest {
|
||||
@ApiProperty({
|
||||
example: 'default',
|
||||
description: 'Session name (id)',
|
||||
})
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => SessionConfig)
|
||||
@IsOptional()
|
||||
config?: SessionConfig;
|
||||
}
|
||||
|
||||
export class SessionStopDeprecatedRequest {
|
||||
@ApiProperty({
|
||||
example: 'default',
|
||||
description: 'Session name (id)',
|
||||
})
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: false,
|
||||
required: false,
|
||||
description: 'Stop and logout from the session.',
|
||||
})
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
logout: boolean | undefined = false;
|
||||
}
|
||||
|
||||
export class SessionLogoutDeprecatedRequest {
|
||||
@ApiProperty({
|
||||
example: 'default',
|
||||
description: 'Session name (id)',
|
||||
})
|
||||
@IsString()
|
||||
name: string;
|
||||
}
|
||||
@@ -1,4 +1,15 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { BooleanString } from '@waha/nestjs/validation/BooleanString';
|
||||
import { IsDynamicObject } from '@waha/nestjs/validation/IsDynamicObject';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
import { WAHASessionStatus } from './enums.dto';
|
||||
import { ChatIdProperty } from './properties.dto';
|
||||
@@ -14,7 +25,10 @@ export class ListSessionsQuery {
|
||||
description:
|
||||
'Return all sessions, including those that are in the STOPPED state.',
|
||||
})
|
||||
all: boolean;
|
||||
@Transform(BooleanString)
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
all?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,16 +38,21 @@ export class ProxyConfig {
|
||||
@ApiProperty({
|
||||
example: 'localhost:3128',
|
||||
})
|
||||
@IsString()
|
||||
server: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: null,
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
username?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: null,
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
password?: string;
|
||||
}
|
||||
|
||||
@@ -42,6 +61,7 @@ export class NowebStoreConfig {
|
||||
description:
|
||||
'Enable or disable the store for contacts, chats, and messages.',
|
||||
})
|
||||
@IsBoolean()
|
||||
enabled: boolean = false;
|
||||
|
||||
@ApiProperty({
|
||||
@@ -50,22 +70,52 @@ export class NowebStoreConfig {
|
||||
'Full sync will download all contacts, chats, and messages from the phone.\n' +
|
||||
'If disabled, only messages early than 90 days will be downloaded and some contacts may be missing.',
|
||||
})
|
||||
@IsBoolean()
|
||||
fullSync: boolean = false;
|
||||
}
|
||||
|
||||
export class NowebConfig {
|
||||
@ValidateNested()
|
||||
@Type(() => NowebStoreConfig)
|
||||
@IsOptional()
|
||||
store?: NowebStoreConfig;
|
||||
}
|
||||
|
||||
export class SessionConfig {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => WebhookConfig)
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
webhooks?: WebhookConfig[];
|
||||
|
||||
@ApiProperty({
|
||||
example: {
|
||||
'user.id': '123',
|
||||
'user.email': 'email@example.com',
|
||||
},
|
||||
description:
|
||||
"Metadata for the session. You'll get 'metadata' in all webhooks.",
|
||||
required: false,
|
||||
})
|
||||
@IsDynamicObject()
|
||||
@IsOptional()
|
||||
metadata?: Map<string, string>;
|
||||
|
||||
@ApiProperty({
|
||||
example: null,
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => ProxyConfig)
|
||||
@IsOptional()
|
||||
proxy?: ProxyConfig;
|
||||
|
||||
debug: boolean = false;
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
default: false,
|
||||
})
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
debug: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
example: {
|
||||
@@ -75,30 +125,20 @@ export class SessionConfig {
|
||||
},
|
||||
},
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => NowebConfig)
|
||||
@IsOptional()
|
||||
noweb?: NowebConfig;
|
||||
}
|
||||
|
||||
export class SessionStartRequest {
|
||||
name = 'default';
|
||||
config?: SessionConfig;
|
||||
}
|
||||
|
||||
export class SessionStopRequest {
|
||||
name = 'default';
|
||||
@ApiProperty({
|
||||
example: false,
|
||||
required: false,
|
||||
description: 'Stop and logout from the session.',
|
||||
})
|
||||
logout = false;
|
||||
}
|
||||
|
||||
export class SessionLogoutRequest {
|
||||
name = 'default';
|
||||
}
|
||||
|
||||
export class SessionDTO {
|
||||
name = 'default';
|
||||
@ApiProperty({
|
||||
example: 'default',
|
||||
description: 'Session name (id)',
|
||||
})
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
status: WAHASessionStatus;
|
||||
config?: SessionConfig;
|
||||
}
|
||||
@@ -114,3 +154,39 @@ export class SessionInfo extends SessionDTO {
|
||||
me?: MeInfo;
|
||||
engine?: any;
|
||||
}
|
||||
|
||||
const MONGO_DB_NAME_LIMIT = 64;
|
||||
const MONGO_DB_NAME_PREFIX_LEN = 'waha_noweb'.length;
|
||||
|
||||
export class SessionCreateRequest {
|
||||
@ApiProperty({
|
||||
example: 'default',
|
||||
description: 'Session name (id)',
|
||||
required: false,
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(MONGO_DB_NAME_LIMIT - MONGO_DB_NAME_PREFIX_LEN)
|
||||
name: string | undefined;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => SessionConfig)
|
||||
@IsOptional()
|
||||
config?: SessionConfig;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Start session after creation',
|
||||
example: true,
|
||||
default: true,
|
||||
})
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
start?: boolean;
|
||||
}
|
||||
|
||||
export class SessionUpdateRequest {
|
||||
@ValidateNested()
|
||||
@Type(() => SessionConfig)
|
||||
@IsOptional()
|
||||
config?: SessionConfig;
|
||||
}
|
||||
@@ -1,25 +1,43 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
export class RetriesConfiguration {
|
||||
@ApiProperty({
|
||||
example: 2,
|
||||
})
|
||||
delaySeconds: number;
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
delaySeconds?: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 15,
|
||||
})
|
||||
attempts: number;
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
attempts?: number;
|
||||
}
|
||||
|
||||
export class CustomHeader {
|
||||
@ApiProperty({
|
||||
example: 'X-My-Custom-Header',
|
||||
})
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Value',
|
||||
})
|
||||
@IsString()
|
||||
value: string;
|
||||
}
|
||||
|
||||
@@ -27,7 +45,9 @@ export class HmacConfiguration {
|
||||
@ApiProperty({
|
||||
example: 'your-secret-key',
|
||||
})
|
||||
key: string;
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export class WebhookConfig {
|
||||
@@ -37,26 +57,39 @@ export class WebhookConfig {
|
||||
description:
|
||||
'You can use https://docs.webhook.site/ to test webhooks and see the payload',
|
||||
})
|
||||
@IsUrl({ require_protocol: true, require_tld: false })
|
||||
url: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: ['message', 'session.status'],
|
||||
required: true,
|
||||
})
|
||||
events: string[];
|
||||
@IsEnum(WAHAEvents, { each: true })
|
||||
@IsArray()
|
||||
events: WAHAEvents[];
|
||||
|
||||
@ApiProperty({
|
||||
example: null,
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => HmacConfiguration)
|
||||
@IsOptional()
|
||||
hmac?: HmacConfiguration;
|
||||
|
||||
@ApiProperty({
|
||||
example: null,
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => RetriesConfiguration)
|
||||
@IsOptional()
|
||||
retries?: RetriesConfiguration;
|
||||
|
||||
@ApiProperty({
|
||||
example: null,
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => CustomHeader)
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
customHeaders?: CustomHeader[];
|
||||
}
|
||||
@@ -106,6 +106,15 @@ export class WAHAWebhook {
|
||||
})
|
||||
session: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: {
|
||||
'user.id': '123',
|
||||
'user.email': 'email@example.com',
|
||||
},
|
||||
description: 'Metadata for the session.',
|
||||
})
|
||||
metadata?: Map<string, string>;
|
||||
|
||||
@ApiProperty({
|
||||
example: WAHAEngine.WEBJS,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const fs = require('fs-extra');
|
||||
|
||||
export async function fileExists(filepath: string) {
|
||||
try {
|
||||
await fs.access(filepath, fs.constants.F_OK);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { v4 as uuid4 } from 'uuid';
|
||||
|
||||
/**
|
||||
* Generate prefix uuid (but remove -)
|
||||
* @param prefix
|
||||
*/
|
||||
export function generatePrefixedId(prefix: string) {
|
||||
const id = uuid4().replace(/-/g, '');
|
||||
return `${prefix}_${id}`;
|
||||
}
|
||||
@@ -31,6 +31,11 @@ export function getPinoLogLevel(debug: boolean = false): Level {
|
||||
return getDefaultPinoLogLevel();
|
||||
}
|
||||
|
||||
export function isDebugEnabled() {
|
||||
const level = getPinoLogLevel();
|
||||
return level == 'debug' || level == 'trace';
|
||||
}
|
||||
|
||||
export function getPinoHttpUseLevel(): LevelWithSilent {
|
||||
const levels = ['fatal', 'error', 'warn', 'info', 'debug', 'trace'];
|
||||
const level = (process.env.WAHA_HTTP_LOG_LEVEL || 'info').toLowerCase();
|
||||
|
||||
@@ -36,3 +36,17 @@ export const promiseTimeout = function (
|
||||
export async function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export async function waitUntil(
|
||||
condition: () => Promise<boolean>,
|
||||
everyMs: number,
|
||||
timeoutMs: number,
|
||||
): Promise<boolean> {
|
||||
const startTime = Date.now();
|
||||
let result = await condition();
|
||||
while (!result && Date.now() - startTime < timeoutMs) {
|
||||
await sleep(everyMs);
|
||||
result = await condition();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
+1
-1
@@ -33,7 +33,7 @@ export function getEngineName(): string {
|
||||
}
|
||||
|
||||
export const VERSION: WAHAEnvironment = {
|
||||
version: '2024.8.3',
|
||||
version: '2024.9.2',
|
||||
engine: getEngineName(),
|
||||
tier: getWAHAVersion(),
|
||||
browser: getBrowserExecutablePath(),
|
||||
|
||||
Reference in new issue
Block a user