Compare commits

...
53 Commits
Author SHA1 Message Date
devlikepro d66c80f95a [core] 2025.8.3
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-08-30 18:30:07 +07:00
devlikepro 26f04bbd94 [core] Up Dashboard 2025-08-30 18:30:06 +07:00
devlikepro 6582c03823 [core] Up GOWS 2025-08-30 18:30:06 +07:00
devlikepro abacaf2e02 [core] WEBJS - fix STARTING session
fix #1298
fix #1299
fix #1300
2025-08-30 18:30:05 +07:00
devlikepro c65d8b9856 [core] GOWS - chat /unread 2025-08-30 18:30:05 +07:00
devlikepro 8f94cf06de [core] WEBJS - add webjs.tagsEventsOn config 2025-08-30 18:30:04 +07:00
devlikepro 94c1c16de8 [core] Use oxlint instead of eslint 2025-08-30 18:30:03 +07:00
devlikepro e69d992725 [core] Ignore - Status, Groups, Broadcast
fix #1142
fix #1259
fix #1190
2025-08-30 18:30:03 +07:00
devlikepro e6c5dd6e22 [core] eslint - no project 2025-08-30 18:30:02 +07:00
devlikepro 5486d4874a [core] Refactor media and processIncomingMessage 2025-08-30 18:30:02 +07:00
devlikepro 9d9d8e03fe [core] Add @types 2025-08-30 18:30:02 +07:00
devlikepro 6ee50243a2 [core] 2025.8.2
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
2025-08-25 10:31:26 +07:00
devlikepro 3ec078f9c1 [core] Update Dashboard 2025-08-25 10:31:26 +07:00
devlikepro 4c3209be6d [core] GOWS v1.0.12 2025-08-25 10:31:26 +07:00
devlikepro 7fe8f9a8dc [core] put "stack" last in errors 2025-08-25 10:31:25 +07:00
devlikepro 7fa2b29418 [core] GOWS - /api/sendPollVote
fix #1077
2025-08-25 10:31:25 +07:00
devlikepro dd4edd5a85 [core] show the engine name in errors 2025-08-25 10:31:24 +07:00
devlikepro b85df5a0d3 [core] GOWS - /api/sendLocation
fix #1250
2025-08-25 10:31:24 +07:00
devlikepro 1a7bec3d4e [core] NOWEB - add title for location, convert to right jid 2025-08-25 10:31:23 +07:00
devlikepro 727c4b454f [core] GOWS - Fix poll key
fix #1028
2025-08-25 10:31:23 +07:00
devlikepro 4ba86b34a9 [core] Add me.lid - GOWS, NOWEB 2025-08-25 10:31:22 +07:00
devlikepro 277a35aed9 [core] Add /api/sendList - NOWEB, GOWS
fix #393
fix #482
fix #715
fix #938
2025-08-25 10:31:21 +07:00
devlikepro 28c1ffa09f [core] ChatWoot - fix link preview for big files issue
fix #1162
2025-08-25 10:31:21 +07:00
devlikepro 10595680a9 [core] Dashboard - add zh - Simplified Chinese
fix #1193
2025-08-25 10:31:21 +07:00
devlikepro 577298dbc5 [core] ChatWoot - add zh-CN - Simplified Chinese
fix #1193
2025-08-25 10:31:21 +07:00
devlikepro 49ca4dfbf7 [core] rm LocaleCode 2025-08-25 10:31:21 +07:00
devlikepro 89ec47deec [core] 2025.8.1
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-08-05 15:53:42 +07:00
devlikepro a043a46457 [core] API - limited characters in the session name
fix #1163
2025-08-05 15:53:42 +07:00
devlikepro eb8e3a0fd1 [core] WEBJS - fix create group
fix #1145
2025-08-05 15:53:41 +07:00
devlikepro fc25df8104 [core] NOWEB - fix channel list
fix #1177
2025-08-05 15:53:41 +07:00
devlikepro d8ffd3e543 [core] 2025.7.11
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-08-03 13:59:23 +07:00
devlikepro ea4ec9310d [core] WEBJS - fix Caught error, dropping value from, event: 'message.ack'
fix #1178
2025-08-03 13:59:22 +07:00
devlikepro 479ec85e0d [core] Dashboard - i18n 2025-08-03 13:59:22 +07:00
devlikepro 3e4b450260 [core] ChatWoot - find chat id - jid, lid, chat id 2025-08-03 13:59:22 +07:00
devlikepro f6a81949d0 [core] ChatWoot - Urdu 2025-08-03 13:59:21 +07:00
devlikepro 6896962606 [core] Always show list of chatwoot languages
fix #1138
2025-08-03 13:59:21 +07:00
devlikepro 9b6d6ab4b0 [core] Inline LOCALE_NAME in yaml 2025-08-03 13:59:21 +07:00
devlikepro d5f399f7d5 [core] 2025.7.10
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-07-27 17:39:17 +07:00
devlikepro dbd3bd024c [core] Up GOWS
Fix #1100
2025-07-27 17:39:16 +07:00
devlikepro 9ba7e19da3 [core] Use sqlite3 instead of better-sqlite3
better-sqlite3 can freeze the whole system
2025-07-27 17:39:16 +07:00
devlikepro 5f6d1412cf [core] Add sqlite3 2025-07-27 17:39:15 +07:00
devlikepro 0b5af3145e [core] Always save config, not null 2025-07-27 17:39:15 +07:00
devlikepro ad10ea4e96 [core] 2025.7.9
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-07-25 12:46:33 +07:00
devlikepro 9de53a972a [core] Up NOWEB
fix #1143
2025-07-25 12:46:33 +07:00
devlikepro 4043efbb9a [core] Up Dashboard 2025-07-25 12:46:33 +07:00
devlikepro 42019644f0 [core] cpus*2 for UV_THREADPOOL_SIZE 2025-07-25 12:46:33 +07:00
devlikepro bcc25ef42f [core] Adjust UV_THREADPOOL_SIZE logic 2025-07-25 12:46:33 +07:00
devlikepro 602bbf3b60 [core] WEBJS - WAHA_RUN_XVFB
fix #1151
2025-07-25 12:46:32 +07:00
devlikeaproandsnyk-bot 585ac6074f [core] fix: package.json to reduce vulnerabilities (#16)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-MULTER-10773732

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2025-07-24 10:37:37 +07:00
devlikepro 5bd1a43df6 [core] 2025.7.8
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-07-23 15:02:05 +07:00
devlikepro c9279a4b59 [core] ChatWoot - send png as images 2025-07-23 15:02:04 +07:00
devlikepro 9ff8eed6ba [core] Add, edit, and remove contact API
fix #1124
2025-07-23 15:02:03 +07:00
devlikepro 96ffdd251f [core] Up GOWS 2025-07-23 15:01:53 +07:00
66 changed files with 5321 additions and 1822 deletions

No files matched your search

+1
View File
@@ -1,2 +1,3 @@
src/core/engines/gows/grpc/*
src/core/engines/gows/proto/*
src/core/engines/webjs/_lodash.js
-36
View File
@@ -1,36 +0,0 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin', 'simple-import-sort'],
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'prettier/@typescript-eslint',
],
root: true,
env: {
node: true,
jest: true,
},
rules: {
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'lines-between-class-members': [
'error',
'always',
{ exceptAfterSingleLine: true },
],
},
};
+15
View File
@@ -0,0 +1,15 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript"],
"env": {
"builtin": true
},
"rules": {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "off",
"no-unused-vars": "off"
}
}
+5 -11
View File
@@ -17,18 +17,12 @@ repos:
language: python
stages: [commit-msg]
- repo: local
hooks:
- id: lint
stages: [pre-commit]
name: lint
- id: oxlint
name: oxlint
entry: npx oxlint --fix --deny-warnings
language: system
entry: bash -c '$HOME/.nvm/nvm-exec npm run lint-cache'
pass_filenames: false
files: |
(?x)^(
src/.*|
)$
pass_filenames: true
files: \.(ts|tsx|js|jsx)$
- repo: https://github.com/pre-commit/mirrors-prettier
rev: 'v3.1.0'
+30 -9
View File
@@ -4,8 +4,18 @@
#
# Calculate UV_THREADPOOL_SIZE based on number of CPUs
#
cpus=$(node -e "const os = require('os'); console.log(os.cpus().length);")
uv_threadpool_size=$(($cpus * 1))
# Try to get CPU count using Node.js, fallback to 1 if it fails
cpus=$(node -e "const os = require('os'); console.log(os.cpus().length);" 2>/dev/null) || cpus=1
# Make sure cpus is a number, default to 1 if not
case $cpus in
''|*[!0-9]*) cpus=1 ;;
esac
uv_threadpool_size=$(($cpus * 2))
# Check if uv_threadpool_size is less than 4 (default), set it to 4 if it is
if [ "$uv_threadpool_size" -lt 4 ]; then
uv_threadpool_size=4
fi
# Set UV_THREADPOOL_SIZE as an environment variable
export UV_THREADPOOL_SIZE="${UV_THREADPOOL_SIZE:-$uv_threadpool_size}"
@@ -46,13 +56,22 @@ fi
# xvfb-run
#
USE_XVFB=false
if [ -z "$WHATSAPP_DEFAULT_ENGINE" ] || [ "$WHATSAPP_DEFAULT_ENGINE" = "WEBJS" ]; then
# Try to run xvfb-run with a test command
if xvfb-run --auto-servernum echo "xvfb-run is working!"; then
USE_XVFB=true
else
echo "xvfb-run test failed, do not run it"
USE_XVFB=false
# Check WAHA_RUN_XVFB parameter - only test for "false" case, treat all others as True
if [ "$WAHA_RUN_XVFB" = "false" ] || [ "$WAHA_RUN_XVFB" = "False" ] || [ "$WAHA_RUN_XVFB" = "0" ]; then
# Explicitly disabled by user
echo "WAHA_RUN_XVFB value: $WAHA_RUN_XVFB - xvfb is disabled"
USE_XVFB=false
else
# Check engine and run test if it's WEBJS or not specified
if [ -z "$WHATSAPP_DEFAULT_ENGINE" ] || [ "$WHATSAPP_DEFAULT_ENGINE" = "WEBJS" ]; then
# Try to run xvfb-run with a test command
if xvfb-run --auto-servernum echo "xvfb-run is working!"; then
USE_XVFB=true
else
echo "xvfb-run test failed, do not run it"
USE_XVFB=false
fi
fi
fi
@@ -60,7 +79,9 @@ fi
# Start your application using node with exec to ensure proper signal handling
#
if [ "$USE_XVFB" = "true" ]; then
echo "Executing node with xvfb-run..."
exec xvfb-run --auto-servernum node dist/main
else
echo "Executing node without xvfb-run..."
exec node dist/main
fi
+8 -9
View File
@@ -14,8 +14,8 @@
"start:debug": "nest start --debug --watch",
"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",
"lint": "oxlint --deny-warnings",
"lint-fix": "oxlint --fix --deny-warnings",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
@@ -41,7 +41,7 @@
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^11.0.0",
"@nestjs/passport": "^11.0.0",
"@nestjs/platform-express": "^11.0.0",
"@nestjs/platform-express": "^11.1.5",
"@nestjs/platform-ws": "^11.0.0",
"@nestjs/serve-static": "^5.0.0",
"@nestjs/swagger": "^7.1.11",
@@ -51,6 +51,7 @@
"@types/lodash": "^4.14.194",
"@types/mustache": "^4.2.5",
"@types/passport": "^1.0.17",
"@types/sqlite3": "^5.1.0",
"@types/ws": "^8.5.4",
"adm-zip": "0.5.10",
"agentkeepalive": "^4.5.0",
@@ -96,6 +97,7 @@
"rimraf": "^4.3.0",
"rxjs": "^7.8.1",
"sharp": "^0.33.4",
"sqlite3": "^5.1.7",
"swagger-ui-express": "^4.1.4",
"ulid": "^2.3.0",
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2025-06-09",
@@ -122,15 +124,12 @@
"@types/express": "^4.17.3",
"@types/jest": "26.0.10",
"@types/node": "^18.14.2",
"@types/node-fetch": "^2.6.13",
"@types/semver": "^7.7.0",
"@types/supertest": "^2.0.8",
"@typescript-eslint/eslint-plugin": "3.9.1",
"@typescript-eslint/parser": "3.9.1",
"eslint": "7.7.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-simple-import-sort": "^10.0.0",
"grpc-tools": "^1.13.0",
"jest": "^29.7.0",
"oxlint": "^1.13.0",
"prettier": "^1.19.1",
"protoc-gen-ts": "^0.8.7",
"supertest": "^4.0.2",
+26 -1
View File
@@ -16,6 +16,7 @@ import {
transformAck,
} from '@waha/structures/chats.dto';
import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto';
import { SendListRequest } from '@waha/structures/chatting.list.dto';
import { SessionManager } from '../core/abc/manager.abc';
import {
@@ -31,6 +32,7 @@ import {
MessageLinkPreviewRequest,
MessageLocationRequest,
MessagePollRequest,
MessagePollVoteRequest,
MessageReactionRequest,
MessageReplyRequest,
MessageStarRequest,
@@ -121,8 +123,9 @@ export class ChattingController {
@Post('/sendButtons')
@ApiOperation({
summary: 'Send buttons (interactive message)',
summary: 'Send buttons message (interactive)',
description: 'Send Buttons',
deprecated: true,
})
@UsePipes(new WAHAValidationPipe())
async sendButtons(@Body() request: SendButtonsRequest) {
@@ -130,6 +133,17 @@ export class ChattingController {
return whatsapp.sendButtons(request);
}
@Post('/sendList')
@ApiOperation({
summary: 'Send a list message (interactive)',
description: 'Send a List message with sections and rows',
})
@UsePipes(new WAHAValidationPipe())
async sendList(@Body() request: SendListRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendList(request);
}
@Post('/forwardMessage')
async forwardMessage(
@Body() request: MessageForwardRequest,
@@ -193,6 +207,17 @@ export class ChattingController {
return whatsapp.sendPoll(request);
}
@Post('/sendPollVote')
@ApiOperation({
summary: 'Vote on a poll',
description: 'Cast vote(s) on an existing poll message',
})
@UsePipes(new WAHAValidationPipe())
async sendPollVote(@Body() request: MessagePollVoteRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendPollVote(request);
}
@Post('/sendLocation')
async sendLocation(@Body() request: MessageLocationRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
+2 -1
View File
@@ -9,6 +9,7 @@ import { BullAuthMiddleware } from '@waha/apps/app_sdk/auth';
import { AppsDisabledService } from '@waha/apps/app_sdk/services/AppsDisabledService';
import { AppsEnabledService } from '@waha/apps/app_sdk/services/AppsEnabledService';
import { AppsService } from '@waha/apps/app_sdk/services/IAppsService';
import { ChatwootLocalesController } from '@waha/apps/chatwoot/api/chatwoot.locales.controller';
import { ChatWootExports } from '@waha/apps/chatwoot/chatwoot.module';
import { parseBool } from '@waha/helpers';
import { RMutexModule } from '@waha/modules/rmutex';
@@ -88,7 +89,7 @@ const AppsDisabled = {
},
],
imports: [],
controllers: [AppsController],
controllers: [AppsController, ChatwootLocalesController],
};
function checkApiKey() {
@@ -17,9 +17,9 @@ export class ChatwootLocalesController {
description: 'Get available languages for Chatwoot app',
})
getLanguages(): LanguageResponse[] {
return Object.entries(LOCALE_NAMES).map(([locale, name]) => ({
name: name,
locale: locale,
return Array.from(LOCALE_NAMES.entries()).map(([locale, name]) => ({
name,
locale,
}));
}
}
@@ -1,6 +1,6 @@
import { Body, Controller, Param, Post } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { GetChatID } from '@waha/apps/chatwoot/client/ids';
import { FindChatID } from '@waha/apps/chatwoot/client/ids';
import { EventName, MessageType } from '@waha/apps/chatwoot/client/types';
import { INBOX_CONTACT_CHAT_ID } from '@waha/apps/chatwoot/const';
import { InboxData } from '@waha/apps/chatwoot/consumers/types';
@@ -43,7 +43,7 @@ export class ChatwootWebhookController {
// Check if it's a command message (sent to the special inbox contact)
const sender = body?.conversation?.meta?.sender;
const chatId = GetChatID(sender);
const chatId = FindChatID(sender);
const isCommandsChat = chatId === INBOX_CONTACT_CHAT_ID;
// Check if it's a deleted message
+13
View File
@@ -15,6 +15,19 @@ export function GetChatID(contact: any): string | null {
return contact?.custom_attributes?.[AttributeKey.WA_CHAT_ID];
}
export function FindChatID(contact: any): string | null {
if (GetJID(contact)) {
return GetJID(contact);
}
if (GetLID(contact)) {
return GetLID(contact);
}
if (GetChatID(contact)) {
return GetChatID(contact);
}
return null;
}
export function SerializeWhatsAppKey(message: WhatsAppMessage): string {
const key = {
id: message.message_id,
+2 -2
View File
@@ -1,7 +1,7 @@
import { AppConsumer } from '@waha/apps/app_sdk/AppConsumer';
import { JobLoggerWrapper } from '@waha/apps/app_sdk/JobLoggerWrapper';
import { HasBeenRetried } from '@waha/apps/app_sdk/JobUtils';
import { GetChatID } from '@waha/apps/chatwoot/client/ids';
import { FindChatID } from '@waha/apps/chatwoot/client/ids';
import { EventName } from '@waha/apps/chatwoot/client/types';
import { ChatWootConversationKey } from '@waha/apps/chatwoot/consumers/mutex';
import { InboxData } from '@waha/apps/chatwoot/consumers/types';
@@ -124,7 +124,7 @@ export async function LookupAndCheckChatId(
body: any,
): Promise<string> {
const sender = body.conversation.meta.sender;
let chatId = GetChatID(sender);
let chatId = FindChatID(sender);
if (!chatId && sender.phone_number) {
const existResult = await session.contactCheckExists(sender.phone_number);
if (!existResult.numberExists) {
@@ -154,6 +154,8 @@ export class MessageHandler {
chatId: chatId,
text: content,
reply_to: replyTo,
linkPreview: false,
linkPreviewHighQuality: false,
};
const session = this.session;
await session.readMessages(chatId);
@@ -180,7 +182,7 @@ export class MessageHandler {
switch (file.file_type) {
case 'image':
if (mimetype != 'image/jpeg') {
if (mimetype != 'image/jpeg' && mimetype != 'image/png') {
// Send it as a file
break;
}
+1
View File
@@ -1,4 +1,5 @@
# Arabic translations for ChatWoot integration
LOCALE_NAME: '🇦🇪 العربية (Arabic)'
APP_INBOX_CONTACT_NAME: 'تكامل واتساب (WAHA)'
JOB_ERROR_REPORT: |
+1
View File
@@ -1,4 +1,5 @@
# Bengali translations for ChatWoot integration
LOCALE_NAME: '🇧🇩 বাংলা (Bengali)'
APP_INBOX_CONTACT_NAME: 'হোয়াটসঅ্যাপ ইন্টিগ্রেশন (WAHA)'
JOB_ERROR_REPORT: |
+1
View File
@@ -1,4 +1,5 @@
# German translations for ChatWoot integration
LOCALE_NAME: '🇩🇪 Deutsch (German)'
APP_INBOX_CONTACT_NAME: 'WhatsApp Integration (WAHA)'
JOB_ERROR_REPORT: |
+1
View File
@@ -1,4 +1,5 @@
# English translations for ChatWoot integration
LOCALE_NAME: '🇺🇸 English (English)'
APP_INBOX_CONTACT_NAME: 'WhatsApp Integration (WAHA)'
APP_INBOX_CONTACT_AVATAR_URL: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
+1
View File
@@ -1,4 +1,5 @@
# Spanish translations for ChatWoot integration
LOCALE_NAME: '🇪🇸 Español (Spanish)'
APP_INBOX_CONTACT_NAME: 'Integración de WhatsApp (WAHA)'
JOB_ERROR_REPORT: |
+1
View File
@@ -1,4 +1,5 @@
# Persian translations for ChatWoot integration
LOCALE_NAME: '🇮🇷 فارسی (Persian)'
APP_INBOX_CONTACT_NAME: 'ادغام واتساپ (WAHA)'
JOB_ERROR_REPORT: |
+1
View File
@@ -1,4 +1,5 @@
# French translations for ChatWoot integration
LOCALE_NAME: '🇫🇷 Français (French)'
APP_INBOX_CONTACT_NAME: 'Intégration WhatsApp (WAHA)'
JOB_ERROR_REPORT: |
+1
View File
@@ -1,4 +1,5 @@
# Hebrew translations for ChatWoot integration
LOCALE_NAME: '🇮🇱 עברית (Hebrew)'
APP_INBOX_CONTACT_NAME: 'אינטגרציית וואטסאפ (WAHA)'
JOB_ERROR_REPORT: |
+1
View File
@@ -1,4 +1,5 @@
# Hindi translations for ChatWoot integration
LOCALE_NAME: '🇮🇳 हिंदी (Hindi)'
APP_INBOX_CONTACT_NAME: 'व्हाट्सएप इंटीग्रेशन (WAHA)'
JOB_ERROR_REPORT: |
+1
View File
@@ -1,4 +1,5 @@
# Indonesian translations for ChatWoot integration
LOCALE_NAME: '🇮🇩 Bahasa Indonesia (Indonesian)'
APP_INBOX_CONTACT_NAME: 'Integrasi WhatsApp (WAHA)'
JOB_ERROR_REPORT: |
+1
View File
@@ -1,4 +1,5 @@
# Punjabi translations for ChatWoot integration
LOCALE_NAME: '🇵🇰 ਪੰਜਾਬੀ (Punjabi)'
APP_INBOX_CONTACT_NAME: 'ਵਟਸਐਪ ਇੰਟੀਗ੍ਰੇਸ਼ਨ (WAHA)'
JOB_ERROR_REPORT: |
+1
View File
@@ -1,4 +1,5 @@
# Portuguese translations for ChatWoot integration
LOCALE_NAME: '🇧🇷🇵🇹 Português (Portuguese)'
APP_INBOX_CONTACT_NAME: 'Integração WhatsApp (WAHA)'
JOB_ERROR_REPORT: |
+1
View File
@@ -1,4 +1,5 @@
# Russian translations for ChatWoot integration
LOCALE_NAME: '🇷🇺 Русский (Russian)'
APP_INBOX_CONTACT_NAME: 'WhatsApp Интеграция (WAHA)'
JOB_ERROR_REPORT: |
+1
View File
@@ -1,4 +1,5 @@
# Turkish translations for ChatWoot integration
LOCALE_NAME: '🇹🇷 Türkçe (Turkish)'
APP_INBOX_CONTACT_NAME: 'WhatsApp Entegrasyonu (WAHA)'
JOB_ERROR_REPORT: |
+1
View File
@@ -1,4 +1,5 @@
# Ukrainian translations for ChatWoot integration
LOCALE_NAME: '🇺🇦 Українська (Ukrainian)'
APP_INBOX_CONTACT_NAME: 'Інтеграція WhatsApp (WAHA)'
JOB_ERROR_REPORT: |
+182
View File
@@ -0,0 +1,182 @@
# Urdu translations for ChatWoot integration
LOCALE_NAME: '🇵🇰 اردو (Urdu)'
APP_INBOX_CONTACT_NAME: 'واٹس ایپ انٹیگریشن (WAHA)'
APP_INBOX_CONTACT_AVATAR_URL: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
JOB_ERROR_REPORT: |
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
🔄 {{attempts.nextDelay}} سیکنڈ میں دوبارہ کوشش کر رہا ہے...
{{/attempts.nextDelay}}
{{^attempts.nextDelay}}
❌ مزید کوششیں نہیں بچی ہیں۔ براہ کرم بعد میں دوبارہ کوشش کریں۔
{{/attempts.nextDelay}}
کوشش: {{{attempts.current}}} / {{{attempts.max}}}
تفصیلات: [{{{details.text}}}]({{{details.url}}})
{{#error}}
```
{{{error}}}
```
{{/error}}
JOB_SUCCEEDED_REPORT: |
✅ **کامیاب ہوا**
کوشش: {{{attempts.current}}} / {{{attempts.max}}}
تفصیلات: [{{{details.text}}}]({{{details.url}}})
WHATSAPP_ERROR_SENDING_MESSAGE: واٹس ایپ پر پیغام نہیں بھیج سکا۔
WHATSAPP_ERROR_RECEIVING_MESSAGE: واٹس ایپ سے پیغام موصول نہیں کر سکا
WHATSAPP_ERROR_REMOVING_MESSAGE: واٹس ایپ سے پیغام ہٹا نہیں سکا
APP_SCHEDULED_JOB_ERROR: شیڈول کردہ کام انجام دینے میں ناکام رہا
MESSAGE_FROM_WHATSAPP: |
{{{text}}}
📱 *واٹس ایپ سے بھیجا گیا*
MESSAGE_FROM_API: |
{{{text}}}
📱 *API کے ذریعے بھیجا گیا*
MESSAGE_REMOVED_IN_WHATSAPP: |
❌ *یہ پیغام واٹس ایپ میں حذف کر دیا گیا تھا*
MESSAGE_EDITED_IN_WHATSAPP: |
{{{text}}}
✏️ *واٹس ایپ میں ترمیم کی گئی*
WHATSAPP_GROUP_MESSAGE: |
{{{text}}}
👥 *{{{participant}}}*
WHATSAPP_REACTION_ADDED: |
*ردعمل دیا* {{{emoji}}}
WHATSAPP_REACTION_REMOVED: |
*ردعمل ہٹا دیا گیا*
WHATSAPP_CONTACT_GROUP_SUFFIX: 'گروپ'
WHATSAPP_CONTACT_CHANNEL_SUFFIX: 'چینل'
# Aka Stories
WHATSAPP_CONTACT_STATUS_NAME: 'سٹیٹس'
APP_CONNECTED_MESSAGE: |
🔗 **منسلک: واٹس ایپ سیشن ChatWoot انبکس سے**
✅ واٹس ایپ سیشن **{{{name}}}** کامیابی سے اس ChatWoot انبکس سے منسلک ہو گیا ہے۔
یہ انبکس اب آپ کے **واٹس ایپ اکاؤنٹ** سے منسلک ہے۔ آپ یہاں سے براہ راست پیغامات بھیج اور موصول کر سکتے ہیں — بالکل واٹس ایپ استعمال کرنے کی طرح، لیکن ChatWoot کے اندر۔
آپ اس انبکس سے منسلک واٹس ایپ سیشن کا انتظام بھی کر سکتے ہیں۔ دستیاب اقدامات دیکھنے کے لیے، اس چیٹ میں **help** بھیجیں۔
📋 عام اقدامات جو آپ کر سکتے ہیں:
- چیک کریں کہ آیا واٹس ایپ سیشن فعال ہے
- واٹس ایپ سیشن کو دوبارہ شروع کریں یا روکیں
- لاگ آؤٹ کریں اور نیا QR کوڈ اسکین کر کے دوبارہ منسلک ہوں
اگر سب کچھ کام کر رہا ہے، تو آپ آزادانہ طور پر چیٹ کر سکیں گے۔ اگر آپ کو انتباہات نظر آتے ہیں، تو آپ کو اپنے واٹس ایپ اکاؤنٹ کو دوبارہ منسلک کرنے کی ضرورت ہو سکتی ہے۔
آئیے شروع کریں! 🚀
APP_DISCONNECTED_MESSAGE: |
🔌 **منقطع: واٹس ایپ سیشن ChatWoot انبکس سے**
❌ واٹس ایپ سیشن **{{{name}}}** اس ChatWoot انبکس سے منقطع ہو گیا ہے۔
آپ اب واٹس ایپ سے پیغامات بھیجنے یا موصول کرنے کے قابل نہیں ہوں گے۔
دوبارہ منسلک ہونے کے لیے، براہ کرم اپنے منتظم سے رابطہ کریں۔
APP_UPDATED_MESSAGE: |
🔄 **اپڈیٹ شدہ: واٹس ایپ سیشن کی ترتیبات**
✅ واٹس ایپ سیشن **{{{name}}}** کی ترتیبات اپڈیٹ کر دی گئی ہیں۔
APP_COMMANDS_LIST: |
آپ درج ذیل کمانڈز میں سے ایک بھیج کر **واٹس ایپ انٹیگریشن** کا انتظام کر سکتے ہیں:
🖥️ سیشنز:
1. **status** - موجودہ سیشن کی حالت حاصل کریں۔
2. **restart** - سیشن کو دوبارہ شروع کریں - یہ آپ کے واٹس ایپ اکاؤنٹ کو دوبارہ منسلک کرنے کی کوشش کرے گا۔
3. **start** - سیشن شروع کریں۔
4. **stop** - سیشن روکیں۔
5. **logout** - سیشن سے لاگ آؤٹ کریں، تاکہ آپ QR کوڈ اسکین کر سکیں اور سیشن کی مرمت کر سکیں۔
6. **qr** - سیشن کے لیے QR کوڈ حاصل کریں۔
7. **screenshot** - سیشن کا اسکرین شاٹ حاصل کریں (صرف **WEBJS** انجن میں دستیاب)۔
8. **help** - دستیاب کمانڈز۔
⚙️ سرور:
- **server status** - سرور ورژن اور حالت کی معلومات حاصل کریں۔
- **server reboot** - سرور کو آرام سے دوبارہ شروع کریں۔
- **server reboot force** - سرور کو زبردستی دوبارہ شروع کریں۔
اس گفتگو میں **کمانڈ** (کیس سے غیر حساس) یا **کمانڈ نمبر** بھیجیں۔
APP_HELP_REMINDER: |
دستیاب کمانڈز کی فہرست حاصل کرنے کے لیے **help** بھیجیں۔
APP_SESSION_STATUS_CHANGE: |
{{{emoji}}} "{{{session}}}" سیشن {{{status}}} حالت میں تبدیل ہو گیا
APP_SESSION_CURRENT_STATUS: |
{{{emoji}}} **{{{session}}}** سیشن کی حالت **{{{status}}}** میں ہے
**واٹس ایپ اکاؤنٹ:**
📱 **{{{name}}} ({{{id}}})**
APP_DASHBOARD_LINK: |
مزید تفصیلات کے لیے [WAHA ڈیش بورڈ]({{{url}}}) ملاحظہ کریں۔
APP_SESSION_STATUS_WORKING: |
✅ یہ ChatWoot انبکس **واٹس ایپ اکاؤنٹ** سے منسلک ہے اور پیغامات بھیجنے اور موصول کرنے کے لیے تیار ہے!
📱 **{{{name}}} ({{{id}}})**
APP_SESSION_STATUS_ERROR: |
⚠️ **یہ ChatWoot انبکس واٹس ایپ اکاؤنٹ سے منسلک ہے لیکن پیغامات بھیجنے اور موصول کرنے کے لیے تیار نہیں ہے!**
براہ کرم سیشن کی حالت چیک کریں اور اسے دوبارہ شروع کرنے کی کوشش کریں یا لاگ آؤٹ کریں اور QR کوڈ دوبارہ اسکین کریں۔
APP_SESSION_SCAN_QR_CODE: |
⚠️ **اس ChatWoot انبکس کے ساتھ واٹس ایپ اکاؤنٹ کو منسلک کرنے کے لیے اپنے فون پر QR کوڈ اسکین کریں!**
APP_SERVER_VERSION_AND_STATUS: |
**ورژن:**
```
{{{version}}}
```
**حالت:**
```
{{{status}}}
```
APP_SERVER_REBOOT: |
سرور آرام سے دوبارہ شروع ہو رہا ہے...
APP_SERVER_REBOOT_FORCE: |
سرور زبردستی دوبارہ شروع ہو رہا ہے...
APP_LOGOUT_SUCCESS: |
✅ اکاؤنٹ کامیابی سے غیر مجاز ہو گیا!
APP_NEW_VERSION_AVAILABLE: |
🔄 **نیا ورژن دستیاب ہے!**
موجودہ ورژن: **{{{currentVersion}}}**
نیا ورژن: **{{{newVersion}}}**
👉 [**🆕 تبدیلیوں کی فہرست**]({{{changelogUrl}}}) میں نیا کیا ہے دیکھیں
APP_CORE_VERSION_USED: |
❤️ **WAHA کی حمایت کریں!**
آپ مفت **WAHA Core** ورژن استعمال کر رہے ہیں۔ اگر آپ کو یہ پروجیکٹ مفید لگتا ہے، تو براہ کرم اس کی ترقی کی حمایت کرنے پر غور کریں۔
آپ کی حمایت ہی وہ واحد طریقہ ہے جس سے ہم اپڈیٹس، فکسز، اور نئی خصوصیات پر کام جاری رکھ سکتے ہیں۔
[**🎁 ہماری حمایت کریں**]({{{supportUrl}}})
+182
View File
@@ -0,0 +1,182 @@
# Simplified Chinese translations for ChatWoot integration
LOCALE_NAME: '🇨🇳 中文(Simplified Chinese)'
APP_INBOX_CONTACT_NAME: 'WhatsApp 集成(WAHA)'
APP_INBOX_CONTACT_AVATAR_URL: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
JOB_ERROR_REPORT: |
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
🔄 将在 {{attempts.nextDelay}} 秒后重试...
{{/attempts.nextDelay}}
{{^attempts.nextDelay}}
❌ 没有更多重试机会。请稍后再试。
{{/attempts.nextDelay}}
尝试:{{{attempts.current}}} / {{{attempts.max}}}
详情:[{{{details.text}}}]({{{details.url}}})
{{#error}}
```
{{{error}}}
```
{{/error}}
JOB_SUCCEEDED_REPORT: |
✅ **成功**
尝试:{{{attempts.current}}} / {{{attempts.max}}}
详情:[{{{details.text}}}]({{{details.url}}})
WHATSAPP_ERROR_SENDING_MESSAGE: 无法在 WhatsApp 中发送消息。
WHATSAPP_ERROR_RECEIVING_MESSAGE: 无法接收来自 WhatsApp 的消息
WHATSAPP_ERROR_REMOVING_MESSAGE: 无法从 WhatsApp 中删除消息
APP_SCHEDULED_JOB_ERROR: 计划任务执行失败
MESSAGE_FROM_WHATSAPP: |
{{{text}}}
📱 *通过 WhatsApp 发送*
MESSAGE_FROM_API: |
{{{text}}}
📱 *通过 API 发送*
MESSAGE_REMOVED_IN_WHATSAPP: |
❌ *此消息已在 WhatsApp 中被删除*
MESSAGE_EDITED_IN_WHATSAPP: |
{{{text}}}
✏️ *已在 WhatsApp 中编辑*
WHATSAPP_GROUP_MESSAGE: |
{{{text}}}
👥 *{{{participant}}}*
WHATSAPP_REACTION_ADDED: |
*已反应* {{{emoji}}}
WHATSAPP_REACTION_REMOVED: |
*已移除反应*
WHATSAPP_CONTACT_GROUP_SUFFIX: '群组'
WHATSAPP_CONTACT_CHANNEL_SUFFIX: '频道'
# Aka Stories
WHATSAPP_CONTACT_STATUS_NAME: '状态'
APP_CONNECTED_MESSAGE: |
🔗 **已连接:WhatsApp 会话到 ChatWoot 收件箱**
✅ WhatsApp 会话 **{{{name}}}** 已成功连接到此 ChatWoot 收件箱。
此收件箱现已链接到你的**WhatsApp 账号**。你可以在这里直接收发消息——就像使用 WhatsApp 一样,但在 ChatWoot 内完成。
你也可以管理与此收件箱关联的 WhatsApp 会话。要查看可用操作,请向此对话发送 **help**。
📋 你可以执行的常见操作:
- 检查 WhatsApp 会话是否活动
- 重启或停止 WhatsApp 会话
- 登出并通过扫描新的二维码重新连接
如果一切正常,你就可以自由聊天。如果你看到警告,可能需要重新连接你的 WhatsApp 账号。
开始吧!🚀
APP_DISCONNECTED_MESSAGE: |
🔌 **已断开:从 ChatWoot 收件箱断开 WhatsApp 会话**
❌ WhatsApp 会话 **{{{name}}}** 已与此 ChatWoot 收件箱断开。
你将无法再从 WhatsApp 发送或接收消息。
如需重新连接,请联系你的管理员。
APP_UPDATED_MESSAGE: |
🔄 **已更新:WhatsApp 会话设置**
✅ WhatsApp 会话 **{{{name}}}** 的设置已更新。
APP_COMMANDS_LIST: |
你可以通过发送以下命令来管理**WhatsApp 集成**:
🖥️ 会话:
1. **status** - 获取当前会话状态。
2. **restart** - 重启会话——将尝试重新连接你的 WhatsApp 账号。
3. **start** - 启动会话。
4. **stop** - 停止会话。
5. **logout** - 登出会话,以便你可以扫描二维码并修复会话。
6. **qr** - 获取该会话的二维码。
7. **screenshot** - 获取会话截图(仅在 **WEBJS** 引擎中可用)。
8. **help** - 查看可用命令。
⚙️ 服务器:
- **server status** - 获取服务器版本和状态信息。
- **server reboot** - 优雅地重启服务器。
- **server reboot force** - 强制重启服务器。
向此对话发送**命令**(不区分大小写)或**命令编号**。
APP_HELP_REMINDER: |
发送 **help** 获取可用命令列表。
APP_SESSION_STATUS_CHANGE: |
{{{emoji}}} "{{{session}}}" 会话已切换为 {{{status}}} 状态
APP_SESSION_CURRENT_STATUS: |
{{{emoji}}} **{{{session}}}** 会话状态为 **{{{status}}}**
**WhatsApp 账号:**
📱 **{{{name}}} ({{{id}}})**
APP_DASHBOARD_LINK: |
访问 [WAHA 控制台]({{{url}}}) 以了解更多信息。
APP_SESSION_STATUS_WORKING: |
✅ 此 ChatWoot 收件箱已链接到**WhatsApp 账号**,可以正常收发消息!
📱 **{{{name}}} ({{{id}}})**
APP_SESSION_STATUS_ERROR: |
⚠️ **此 ChatWoot 收件箱已链接到 WhatsApp 账号,但尚未准备好收发消息!**
请检查会话状态,尝试重启,或登出后重新扫描二维码。
APP_SESSION_SCAN_QR_CODE: |
⚠️ **请在手机上扫描二维码,将 WhatsApp 账号与此 ChatWoot 收件箱关联!**
APP_SERVER_VERSION_AND_STATUS: |
**版本:**
```
{{{version}}}
```
**状态:**
```
{{{status}}}
```
APP_SERVER_REBOOT: |
服务器正在优雅重启...
APP_SERVER_REBOOT_FORCE: |
服务器正在强制重启...
APP_LOGOUT_SUCCESS: |
✅ 账号已成功取消授权!
APP_NEW_VERSION_AVAILABLE: |
🔄 **有新版本可用!**
当前版本:**{{{currentVersion}}}**
新版本:**{{{newVersion}}}**
👉 查看[**🆕 更新日志**]({{{changelogUrl}}})
APP_CORE_VERSION_USED: |
❤️ **支持 WAHA!**
你正在使用免费的 **WAHA Core** 版本。如果你觉得该项目有帮助,请考虑支持其开发。
你的支持是我们持续进行更新、修复和新增功能的唯一动力。
[**🎁 支持我们**]({{{supportUrl}}})
+22 -50
View File
@@ -4,59 +4,26 @@ import * as Mustache from 'mustache';
import * as path from 'path';
import * as yaml from 'yaml';
export enum LocaleCode {
EN_US = 'en-US',
AR_AE = 'ar-AE',
BN_BD = 'bn-BD',
PT_BR = 'pt-BR',
DE_DE = 'de-DE',
ES_ES = 'es-ES',
FR_FR = 'fr-FR',
ID_ID = 'id-ID',
HE_IL = 'he-IL',
HI_IN = 'hi-IN',
FA_IR = 'fa-IR',
PA_PK = 'pa-PK',
RU_RU = 'ru-RU',
TR_TR = 'tr-TR',
UK_UA = 'uk-UA',
}
export const LOCALES = [
LocaleCode.EN_US,
LocaleCode.AR_AE,
LocaleCode.BN_BD,
LocaleCode.PT_BR,
LocaleCode.DE_DE,
LocaleCode.ES_ES,
LocaleCode.FR_FR,
LocaleCode.ID_ID,
LocaleCode.HE_IL,
LocaleCode.HI_IN,
LocaleCode.FA_IR,
LocaleCode.PA_PK,
LocaleCode.RU_RU,
LocaleCode.TR_TR,
LocaleCode.UK_UA,
'en-US',
'ar-AE',
'bn-BD',
'pt-BR',
'de-DE',
'es-ES',
'fr-FR',
'id-ID',
'he-IL',
'hi-IN',
'fa-IR',
'pa-PK',
'ru-RU',
'tr-TR',
'uk-UA',
'ur-PK',
'zh-CN',
];
export const LOCALE_NAMES: Record<LocaleCode, string> = {
[LocaleCode.EN_US]: '🇺🇸 English (English)',
[LocaleCode.AR_AE]: '🇦🇪 العربية (Arabic)',
[LocaleCode.BN_BD]: '🇧🇩 বাংলা (Bengali)',
[LocaleCode.PT_BR]: '🇧🇷🇵🇹 Português (Portuguese)',
[LocaleCode.DE_DE]: '🇩🇪 Deutsch (German)',
[LocaleCode.ES_ES]: '🇪🇸 Español (Spanish)',
[LocaleCode.FR_FR]: '🇫🇷 Français (French)',
[LocaleCode.ID_ID]: '🇮🇩 Bahasa Indonesia (Indonesian)',
[LocaleCode.HE_IL]: '🇮🇱 עברית (Hebrew)',
[LocaleCode.HI_IN]: '🇮🇳 हिंदी (Hindi)',
[LocaleCode.FA_IR]: '🇮🇷 فارسی (Persian)',
[LocaleCode.PA_PK]: '🇵🇰 ਪੰਜਾਬੀ (Punjabi)',
[LocaleCode.RU_RU]: '🇷🇺 Русский (Russian)',
[LocaleCode.TR_TR]: '🇹🇷 Türkçe (Turkish)',
[LocaleCode.UK_UA]: '🇺🇦 Українська (Ukrainian)',
};
export const DEFAULT_LOCALE = 'en-US';
function loadTemplateStrings(locale: string) {
@@ -82,8 +49,13 @@ function loadLocales(locales: string[]) {
}
let locales: Record<string, Record<string, string>> = {};
export const LOCALE_NAMES: Map<string, string> = new Map();
try {
locales = loadLocales(LOCALES);
for (const [locale, strings] of Object.entries(locales)) {
LOCALE_NAMES.set(locale, strings['LOCALE_NAME']);
}
} catch (error) {
console.error('Error loading locales:', error);
process.exit(1);
+1 -1
View File
@@ -20,7 +20,7 @@ export function MarkdownToWhatsApp(text: string): string {
// Links: [text](url) → text (url)
.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '$1 ($2)')
// Lists: -, +, * → * item
.replace(/^[\-\+\*] (.*)/gm, '* $1')
.replace(/^[-+*] (.*)/gm, '* $1')
);
}
+18
View File
@@ -1,6 +1,7 @@
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { GlobalWebhookConfigConfig } from '@waha/core/config/GlobalWebhookConfig';
import { IgnoreJidConfig } from '@waha/core/utils/jids';
import { parseBool } from './helpers';
import { WebhookConfig } from './structures/webhooks.config.dto';
@@ -178,6 +179,23 @@ export class WhatsappConfigService implements OnApplicationBootstrap {
return parseBool(value);
}
/**
* Global default "ignore settings" for chats.
* If not defined, defaults to false (do not ignore anything).
*/
getIgnoreChatsConfig(): IgnoreJidConfig {
const status = parseBool(
this.configService.get('WAHA_SESSION_CONFIG_IGNORE_STATUS', 'false'),
);
const groups = parseBool(
this.configService.get('WAHA_SESSION_CONFIG_IGNORE_GROUPS', 'false'),
);
const channels = parseBool(
this.configService.get('WAHA_SESSION_CONFIG_IGNORE_CHANNELS', 'false'),
);
return { status, groups, channels };
}
onApplicationBootstrap() {
const error = this.webhookConfig.validateConfig();
if (error) {
+7
View File
@@ -0,0 +1,7 @@
import { WAHAEngine } from '@waha/structures/enums.dto';
export function getEngineName(): string {
// Load engine name from WHATSAPP_DEFAULT_ENGINE environment variable
// If not set - use WEBJS
return process.env.WHATSAPP_DEFAULT_ENGINE || WAHAEngine.WEBJS;
}
+38
View File
@@ -0,0 +1,38 @@
import { Body, Controller, Delete, Param, Put, UsePipes } from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import { ChatIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
import { SessionManager } from './core/abc/manager.abc';
import { Result } from './structures/base.dto';
import { ContactUpdateBody } from './structures/contacts.dto';
@ApiSecurity('api_key')
@Controller('api/:session/contacts')
@ApiTags('👤 Contacts')
export class ContactsSessionController {
constructor(private manager: SessionManager) {}
@Put('/:chatId')
@SessionApiParam
@ChatIdApiParam
@ApiOperation({
summary: 'Create or update contact',
description:
'Create or update contact on the phone address book. May not work if you have installed many WhatsApp apps on the same phone',
})
@UsePipes(new WAHAValidationPipe())
async put(
@WorkingSessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
@Body() body: ContactUpdateBody,
): Promise<Result> {
await session.upsertContact(chatId, body);
return { success: true };
}
}
+8
View File
@@ -14,9 +14,11 @@ import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigServi
import { GowsBootstrap } from '@waha/core/engines/gows/GowsBootstrap';
import { ISessionMeRepository } from '@waha/core/storage/ISessionMeRepository';
import { ISessionWorkerRepository } from '@waha/core/storage/ISessionWorkerRepository';
import { IgnoreJidConfig } from '@waha/core/utils/jids';
import { WAHAWebhook } from '@waha/structures/webhooks.dto';
import { waitUntil } from '@waha/utils/promiseTimeout';
import { VERSION } from '@waha/version';
import * as lodash from 'lodash';
import { PinoLogger } from 'nestjs-pino';
import { merge, Observable, of } from 'rxjs';
@@ -235,6 +237,12 @@ export abstract class SessionManager
}
return new NoopEngineBootstrap();
}
protected ignoreChatsConfig(config: SessionConfig) {
const ignore: IgnoreJidConfig = this.config.getIgnoreChatsConfig();
// Given the default, overwrite from the config if any
return lodash.defaults({}, config?.ignore, ignore);
}
}
export function populateSessionInfo(
+35 -6
View File
@@ -4,7 +4,11 @@ import {
IMediaConverter,
} from '@waha/core/media/IConverter';
import { MessagesForRead } from '@waha/core/utils/convertors';
import { isJidNewsletter } from '@waha/core/utils/jids';
import {
IgnoreJidConfig,
isJidNewsletter,
JidFilter,
} from '@waha/core/utils/jids';
import {
Channel,
ChannelListResult,
@@ -25,6 +29,7 @@ import {
ReadChatMessagesResponse,
} from '@waha/structures/chats.dto';
import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto';
import { SendListRequest } from '@waha/structures/chatting.list.dto';
import { BinaryFile, RemoteFile } from '@waha/structures/files.dto';
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
@@ -69,6 +74,7 @@ import {
MessageLinkPreviewRequest,
MessageLocationRequest,
MessagePollRequest,
MessagePollVoteRequest,
MessageReactionRequest,
MessageReplyRequest,
MessageStarRequest,
@@ -77,17 +83,18 @@ import {
MessageVoiceRequest,
SendSeenRequest,
} from '../../structures/chatting.dto';
import { ContactQuery, ContactRequest } from '../../structures/contacts.dto';
import {
ContactQuery,
ContactRequest,
ContactUpdateBody,
} from '../../structures/contacts.dto';
import {
WAHAEngine,
WAHAEvents,
WAHAPresenceStatus,
WAHASessionStatus,
} from '../../structures/enums.dto';
import {
EventCancelRequest,
EventMessageRequest,
} from '../../structures/events.dto';
import { EventMessageRequest } from '../../structures/events.dto';
import {
CreateGroupRequest,
GroupField,
@@ -147,8 +154,11 @@ export interface SessionParams {
loggerBuilder: LoggerBuilder;
sessionStore: DataStore;
proxyConfig?: ProxyConfig;
// Raw unchanged SessionConfig
sessionConfig?: SessionConfig;
engineConfig?: any;
// Ignore settings
ignore: IgnoreJidConfig;
}
export abstract class WhatsappSession {
@@ -163,6 +173,7 @@ export abstract class WhatsappSession {
public sessionConfig?: SessionConfig;
protected engineConfig?: any;
protected unpairing: boolean = false;
protected jids: JidFilter;
private _status: WAHASessionStatus;
private shouldPrintQR: boolean;
@@ -189,6 +200,7 @@ export abstract class WhatsappSession {
mediaManager,
sessionConfig,
engineConfig,
ignore,
}: SessionParams) {
this.status$ = new BehaviorSubject(null);
@@ -254,6 +266,11 @@ export abstract class WhatsappSession {
this.sessionConfig = sessionConfig;
this.engineConfig = engineConfig;
this.shouldPrintQR = printQR;
this.logger.info(
{ ignore: ignore },
'The session ignores the following chat ids',
);
this.jids = new JidFilter(ignore);
}
public getEventObservable(event: WAHAEvents) {
@@ -447,6 +464,10 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
sendPollVote(request: MessagePollVoteRequest) {
throw new NotImplementedByEngineError();
}
abstract sendLocation(request: MessageLocationRequest);
sendLinkPreview(request: MessageLinkPreviewRequest) {
@@ -475,6 +496,10 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
sendList(request: SendListRequest): Promise<any> {
throw new NotImplementedByEngineError();
}
sendButtonsReply(request: MessageButtonReply) {
throw new NotImplementedByEngineError();
}
@@ -639,6 +664,10 @@ export abstract class WhatsappSession {
/**
* Contacts methods
*/
public upsertContact(chatId: string, body: ContactUpdateBody): Promise<void> {
throw new NotImplementedByEngineError();
}
public getContact(query: ContactQuery) {
throw new NotImplementedByEngineError();
}
+2
View File
@@ -14,6 +14,7 @@ import { ServerController } from '@waha/api/server.controller';
import { ServerDebugController } from '@waha/api/server.debug.controller';
import { WebsocketGatewayCore } from '@waha/api/websocket.gateway.core';
import { AppsModuleExports } from '@waha/apps/apps.module';
import { ContactsSessionController } from '@waha/contacts.session.controller';
import { ApiKeyStrategy } from '@waha/core/auth/apiKey.strategy';
import { IApiKeyAuth } from '@waha/core/auth/auth';
import { AuthMiddleware } from '@waha/core/auth/auth.middleware';
@@ -146,6 +147,7 @@ export const CONTROLLERS = [
StatusController,
LabelsController,
ContactsController,
ContactsSessionController,
LidsController,
GroupsController,
PresenceController,
+1 -3
View File
@@ -32,9 +32,7 @@ export class HashAuth implements IApiKeyAuth {
constructor(
private hash: string,
private algorithm: string,
) {
this.algorithm = algorithm;
}
) {}
isValid(plain: string): boolean {
if (!plain) {
File diff suppressed because it is too large. Load diff
+69 -22
View File
@@ -48,6 +48,17 @@ function deserialize_messages_ChatPresenceRequest(buffer_arg) {
return gows_pb.ChatPresenceRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_ChatUnreadRequest(arg) {
if (!(arg instanceof gows_pb.ChatUnreadRequest)) {
throw new Error('Expected argument of type messages.ChatUnreadRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_ChatUnreadRequest(buffer_arg) {
return gows_pb.ChatUnreadRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_CheckPhonesRequest(arg) {
if (!(arg instanceof gows_pb.CheckPhonesRequest)) {
throw new Error('Expected argument of type messages.CheckPhonesRequest');
@@ -631,6 +642,17 @@ function deserialize_messages_SubscribePresenceRequest(buffer_arg) {
return gows_pb.SubscribePresenceRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_UpdateContactRequest(arg) {
if (!(arg instanceof gows_pb.UpdateContactRequest)) {
throw new Error('Expected argument of type messages.UpdateContactRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_UpdateContactRequest(buffer_arg) {
return gows_pb.UpdateContactRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_UpdateParticipantsRequest(arg) {
if (!(arg instanceof gows_pb.UpdateParticipantsRequest)) {
throw new Error('Expected argument of type messages.UpdateParticipantsRequest');
@@ -1042,6 +1064,17 @@ getProfilePicture: {
responseSerialize: serialize_messages_CheckPhonesResponse,
responseDeserialize: deserialize_messages_CheckPhonesResponse,
},
markChatUnread: {
path: '/messages.MessageService/MarkChatUnread',
requestStream: false,
responseStream: false,
requestType: gows_pb.ChatUnreadRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_ChatUnreadRequest,
requestDeserialize: deserialize_messages_ChatUnreadRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
//
// Message
//
@@ -1294,6 +1327,42 @@ getLabels: {
responseDeserialize: deserialize_messages_JsonList,
},
//
// Contacts
//
updateContact: {
path: '/messages.MessageService/UpdateContact',
requestStream: false,
responseStream: false,
requestType: gows_pb.UpdateContactRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_UpdateContactRequest,
requestDeserialize: deserialize_messages_UpdateContactRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
getContacts: {
path: '/messages.MessageService/GetContacts',
requestStream: false,
responseStream: false,
requestType: gows_pb.GetContactsRequest,
responseType: gows_pb.JsonList,
requestSerialize: serialize_messages_GetContactsRequest,
requestDeserialize: deserialize_messages_GetContactsRequest,
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
getContactById: {
path: '/messages.MessageService/GetContactById',
requestStream: false,
responseStream: false,
requestType: gows_pb.EntityByIdRequest,
responseType: gows_pb.Json,
requestSerialize: serialize_messages_EntityByIdRequest,
requestDeserialize: deserialize_messages_EntityByIdRequest,
responseSerialize: serialize_messages_Json,
responseDeserialize: deserialize_messages_Json,
},
//
// Events
//
cancelEventMessage: {
@@ -1346,28 +1415,6 @@ getMessageById: {
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
getContacts: {
path: '/messages.MessageService/GetContacts',
requestStream: false,
responseStream: false,
requestType: gows_pb.GetContactsRequest,
responseType: gows_pb.JsonList,
requestSerialize: serialize_messages_GetContactsRequest,
requestDeserialize: deserialize_messages_GetContactsRequest,
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
getContactById: {
path: '/messages.MessageService/GetContactById',
requestStream: false,
responseStream: false,
requestType: gows_pb.EntityByIdRequest,
responseType: gows_pb.Json,
requestSerialize: serialize_messages_EntityByIdRequest,
requestDeserialize: deserialize_messages_EntityByIdRequest,
responseSerialize: serialize_messages_Json,
responseDeserialize: deserialize_messages_Json,
},
getChats: {
path: '/messages.MessageService/GetChats',
requestStream: false,
File diff suppressed because it is too large. Load diff
+182 -40
View File
@@ -5,6 +5,7 @@ import {
isJidGroup,
jidNormalizedUser,
normalizeMessageContent,
proto,
WAMessageKey,
} from '@adiwajshing/baileys';
import { isJidBroadcast } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
@@ -34,6 +35,7 @@ import {
} from '@waha/core/engines/gows/helpers';
import { GowsAuthFactoryCore } from '@waha/core/engines/gows/store/GowsAuthFactoryCore';
import {
extractBody,
getDestination,
toCusFormat,
} from '@waha/core/engines/noweb/session.noweb.core';
@@ -79,6 +81,7 @@ import {
MessageLinkCustomPreviewRequest,
MessageLocationRequest,
MessagePollRequest,
MessagePollVoteRequest,
MessageReactionRequest,
MessageReplyRequest,
MessageTextRequest,
@@ -86,7 +89,8 @@ import {
SendSeenRequest,
WANumberExistResult,
} from '@waha/structures/chatting.dto';
import { ContactQuery } from '@waha/structures/contacts.dto';
import { SendListRequest } from '@waha/structures/chatting.list.dto';
import { ContactQuery, ContactUpdateBody } from '@waha/structures/contacts.dto';
import {
ACK_UNKNOWN,
WAHAEvents,
@@ -95,7 +99,6 @@ import {
WAMessageAck,
} from '@waha/structures/enums.dto';
import {
EventCancelRequest,
EventMessageRequest,
EventResponse,
EventResponsePayload,
@@ -167,6 +170,7 @@ import {
isLabelChatAddedEvent,
isLabelUpsertEvent,
} from './labels.gows';
import IMessageKey = proto.IMessageKey;
enum WhatsMeowEvent {
CONNECTED = 'gows.ConnectedEventData',
@@ -250,6 +254,11 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
proxy: new messages.SessionProxyConfig({
url: this.getProxyUrl(this.proxyConfig),
}),
ignore: new messages.SessionIgnoreJidsConfig({
status: this.jids.ignore.status,
groups: this.jids.ignore.groups,
newsletters: this.jids.ignore.channels,
}),
}),
});
@@ -306,6 +315,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
this.me = {
id: toCusFormat(jidNormalizedUser(data.ID)),
pushName: data.PushName,
lid: jidNormalizedUser(data.LID),
};
// @ts-ignore
this.me.jid = data.ID;
@@ -386,7 +396,11 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
const all$ = this.all$;
this.events2.get(WAHAEvents.ENGINE_EVENT).switch(all$);
const messages$ = all$.pipe(onlyEvent(WhatsMeowEvent.MESSAGE));
const messages$ = all$.pipe(
onlyEvent(WhatsMeowEvent.MESSAGE),
filter((msg: any) => this.jids.include(msg?.Info?.Chat)),
share(),
);
let [messagesFromMe$, messagesFromOthers$] = partition(messages$, isMine);
messagesFromMe$ = messagesFromMe$.pipe(
@@ -443,7 +457,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
const waMessage = await this.toWAMessage(message);
// Extract the body from editedMessage using extractBody function
const body =
this.extractBody(message.Message.protocolMessage.editedMessage) || '';
extractBody(message.Message.protocolMessage.editedMessage) || '';
// Extract the original message ID from protocolMessage.key
const editedMessageId = message.Message.protocolMessage.key?.ID;
return {
@@ -456,7 +470,10 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
);
this.events2.get(WAHAEvents.MESSAGE_EDITED).switch(messagesEdited$);
const receipt$ = all$.pipe(onlyEvent(WhatsMeowEvent.RECEIPT));
const receipt$ = all$.pipe(
onlyEvent(WhatsMeowEvent.RECEIPT),
filter((r: any) => this.jids.include(r?.Chat)),
);
const messageAck$ = receipt$.pipe(
mergeMap(this.receiptToMessageAck.bind(this)),
DistinctAck(),
@@ -471,9 +488,13 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
const presence$ = all$.pipe(
onlyEvent(WhatsMeowEvent.PRESENCE),
filter((event: any) => this.jids.include(event?.From)),
filter((event) => !isJidGroup(event.From)),
);
const chatPresence$ = all$.pipe(onlyEvent(WhatsMeowEvent.CHAT_PRESENCE));
const chatPresence$ = all$.pipe(
onlyEvent(WhatsMeowEvent.CHAT_PRESENCE),
filter((event: any) => this.jids.include(event?.Chat)),
);
const presenceUpdates$ = merge(presence$, chatPresence$).pipe(
map((event) => this.toWahaPresences(event.From || event.Chat, [event])),
);
@@ -533,8 +554,10 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
//
const pollVoteEvent$ = all$.pipe(
onlyEvent(WhatsMeowEvent.POLL_VOTE_EVENT),
filter((event: any) => this.jids.include(event?.Info?.Chat)),
map(this.toPollVotePayload.bind(this)),
filter(Boolean),
share(),
);
// Split into successful and failed responses
@@ -551,6 +574,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
//
const eventMessageResponse$ = all$.pipe(
onlyEvent(WhatsMeowEvent.EVENT_MESSAGE_RESPONSE),
filter((event: any) => this.jids.include(event?.Info?.Chat)),
map(this.toEventResponsePayload.bind(this)),
filter(Boolean),
);
@@ -791,6 +815,14 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
return this.messageResponse(jid, data);
}
sendPollVote(request: MessagePollVoteRequest) {
throw new AvailableInPlusVersion('Poll voting');
}
sendList(request: SendListRequest): Promise<any> {
throw new AvailableInPlusVersion();
}
public async deleteMessage(chatId: string, messageId: string) {
const jid = toJID(this.ensureSuffix(chatId));
const key = parseMessageIdSerialized(messageId);
@@ -880,8 +912,21 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
};
}
sendLocation(request: MessageLocationRequest) {
throw new Error('Method not implemented.');
async sendLocation(request: MessageLocationRequest) {
const jid = toJID(this.ensureSuffix(request.chatId));
const message = new messages.MessageRequest({
jid: jid,
session: this.session,
replyTo: getMessageIdFromSerialized(request.reply_to),
location: new messages.Location({
name: request.title,
degreesLatitude: request.latitude,
degreesLongitude: request.longitude,
}),
});
const response = await promisify(this.client.SendMessage)(message);
const data = response.toObject();
return this.messageResponse(jid, data);
}
forwardMessage(request: MessageForwardRequest): Promise<WAMessage> {
@@ -1464,6 +1509,18 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
* Contacts methods
*/
public async upsertContact(chatId: string, body: ContactUpdateBody) {
const jid = toJID(chatId);
const request = new messages.UpdateContactRequest({
session: this.session,
jid: jid,
firstName: body.firstName || '',
lastName: body.lastName || '',
});
const response = await promisify(this.client.UpdateContact)(request);
response.toObject();
}
protected toWAContact(contact) {
return {
id: toCusFormat(contact.Jid),
@@ -1802,6 +1859,17 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
return labels.map(this.toLabel);
}
public async chatsUnreadChat(chatId: string): Promise<any> {
const jid = toJID(this.ensureSuffix(chatId));
const request = new messages.ChatUnreadRequest({
session: this.session,
jid: jid,
read: false,
});
await promisify(this.client.MarkChatUnread)(request);
return { success: true };
}
public async putLabelsToChat(chatId: string, labels: LabelID[]) {
const jid = toJID(chatId);
const labelsIds = labels.map((label) => label.id);
@@ -1831,7 +1899,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
// END - Methods for API
//
protected async processIncomingMessage(message, downloadMedia = true) {
protected shouldProcessIncomingMessage(message): boolean {
// if there is no text or media message
if (!message) return;
if (!message.Message) return;
@@ -1850,27 +1918,48 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
// Ignore key distribution messages
if (message?.Message?.senderKeyDistributionMessage) return;
}
if (downloadMedia) {
try {
message = await this.downloadMedia(message);
} catch (e) {
this.logger.error('Failed when tried to download media for a message');
this.logger.error(e, e.stack);
}
}
return this.toWAMessage(message);
return true;
}
protected downloadMedia(message) {
protected async processIncomingMessage(message, downloadMedia = true) {
// Filter
if (!this.shouldProcessIncomingMessage(message)) {
return null;
}
// Convert
const wamessage = this.toWAMessage(message);
// Media
if (downloadMedia) {
const media = await this.downloadMediaSafe(message);
wamessage.media = media;
}
return wamessage;
}
protected async downloadMediaSafe(message) {
try {
return await this.downloadMedia(message);
} catch (e) {
this.logger.error('Failed when tried to download media for a message');
this.logger.error(e, e.stack);
return null;
}
}
protected async downloadMedia(message) {
const processor = new GOWSEngineMediaProcessor(this);
return this.mediaManager.processMedia(processor, message, this.name);
const media = await this.mediaManager.processMedia(
processor,
message,
this.name,
);
return media;
}
protected toWAMessage(message): WAMessage {
const fromToParticipant = getFromToParticipant(message);
const id = buildMessageId(message);
const body = this.extractBody(message.Message);
const body = extractBody(message.Message);
const replyTo = this.extractReplyTo(message.Message);
let ack = statusToAck(message.Status);
if (ack === WAMessageAck.ERROR) {
@@ -1892,7 +1981,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
participant: toCusFormat(fromToParticipant.participant),
// Media
hasMedia: Boolean(mediaContent),
media: message.media || null,
media: null,
mediaUrl: message.media?.url,
// @ts-ignore
ack: ack,
@@ -1906,12 +1995,19 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
private toPollVotePayload(event: any): PollVotePayload {
// Extract event creation message key from the message
const creationKey = event.Message?.pollUpdateMessage.pollCreationMessageKey;
const key: WAMessageKey = {
const pollKey: IMessageKey = {
remoteJid: creationKey.remoteJID,
fromMe: creationKey.fromMe,
id: creationKey.ID,
participant: creationKey.participant,
};
const voteKey: IMessageKey = {
id: event.Info.ID,
remoteJid: event.Info.Chat,
participant: event.IsGroup ? event.Info.Sender : null,
fromMe: event.IsFromMe,
};
const key = fixPollCreationKey(voteKey, pollKey, this.me);
const fromToParticipant = getFromToParticipant(event);
const pollCreationKey = getDestination(key);
return {
@@ -1971,21 +2067,6 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
return sentDeviceId === myDeviceId ? MessageSource.API : MessageSource.APP;
}
private extractBody(message) {
if (!message) {
return null;
}
let body = message.Conversation || message.conversation;
if (!body) {
body = message.extendedTextMessage?.text;
}
if (!body) {
const media = extractMediaContent(message);
body = media?.caption;
}
return body;
}
protected extractReplyTo(message): ReplyToMessage | null {
const msgType = getContentType(message);
const contextInfo = message[msgType]?.contextInfo;
@@ -1996,7 +2077,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
if (!quotedMessage) {
return null;
}
const body = this.extractBody(quotedMessage);
const body = extractBody(quotedMessage);
return {
id: contextInfo.stanzaID,
participant: toCusFormat(contextInfo.participant),
@@ -2178,6 +2259,7 @@ function getFromToParticipant(message) {
from: info.Chat,
to: info.IsGroup ? info.Sender : null,
participant: info.IsGroup ? info.Sender : null,
fromMe: info.IsFromMe,
};
}
@@ -2230,3 +2312,63 @@ export function getMessageIdFromSerialized(serialized: string): string | null {
const key = parseMessageIdSerialized(serialized, true);
return key.id;
}
/**
* Poll creation keys are a bit tricky — they contain the data that the receiver uses.
* When recipients respond to a poll, they send back THEIR `message.id`.
*
* For example,
* - If we send a poll and in our system the ID is "true_{chatId}_{ID}",
* - The corresponding `pollCreationKey` will be "false_{ourId}_{ID}" —
* essentially the opposite of our message ID.
*
* The function inspects the key and,
* if it originates from us (based on `remoteJid` or `participant`),
* adjusts it to the correct value.
*/
function fixPollCreationKey(
vote: IMessageKey,
poll: IMessageKey,
me: MeInfo,
): IMessageKey {
// If the vote is from me, the pollCreationKey is already in my perspective
if (vote?.fromMe) {
return poll;
}
if (!me) {
return poll;
}
if (!poll) {
return poll;
}
// DM - my poll, not my vote
if (
toCusFormat(poll.remoteJid) == toCusFormat(me.id) ||
toCusFormat(poll.remoteJid) == toCusFormat(me.lid)
) {
return {
id: poll.id,
remoteJid: vote.remoteJid,
fromMe: true,
participant: undefined,
};
}
// Many Participants Chat - my poll, not my vote
if (
toCusFormat(poll.participant) == toCusFormat(me.id) ||
toCusFormat(poll.participant) == toCusFormat(me.lid)
) {
return {
id: poll.id,
remoteJid: vote.remoteJid,
fromMe: true,
participant: me.id,
};
}
// Other
return poll;
}
+112 -22
View File
@@ -16,7 +16,6 @@ import makeWASocket, {
jidNormalizedUser,
makeCacheableSignalKeyStore,
MiscMessageGenerationOptions,
NewsletterMetadata,
normalizeMessageContent,
PresenceData,
proto,
@@ -121,7 +120,12 @@ import {
SendSeenRequest,
WANumberExistResult,
} from '@waha/structures/chatting.dto';
import { ContactQuery, ContactRequest } from '@waha/structures/contacts.dto';
import { SendListRequest } from '@waha/structures/chatting.list.dto';
import {
ContactQuery,
ContactRequest,
ContactUpdateBody,
} from '@waha/structures/contacts.dto';
import {
ACK_UNKNOWN,
SECOND,
@@ -144,6 +148,7 @@ import {
LabelID,
} from '@waha/structures/labels.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import { WAMedia } from '@waha/structures/media.dto';
import { ReplyToMessage } from '@waha/structures/message.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import {
@@ -380,6 +385,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
this.store = new NowebPersistentStore(
this.loggerBuilder.child({ name: NowebPersistentStore.name }),
storage,
this.jids,
);
await this.store.init();
}
@@ -731,6 +737,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return {
id: toCusFormat(meId),
pushName: me.name,
lid: me.lid,
};
}
@@ -948,15 +955,21 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
);
}
sendList(request: SendListRequest): Promise<any> {
throw new AvailableInPlusVersion();
}
async sendLocation(request: MessageLocationRequest) {
const chatId = toJID(this.ensureSuffix(request.chatId));
const msg = {
location: {
name: request.title || null,
degreesLatitude: request.latitude,
degreesLongitude: request.longitude,
},
};
const options = await this.getMessageOptions(request);
return await this.sock.sendMessage(request.chatId, msg, options);
return await this.sock.sendMessage(chatId, msg, options);
}
async forwardMessage(request: MessageForwardRequest): Promise<WAMessage> {
@@ -1322,6 +1335,28 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
/**
* Contacts methods
*/
public async upsertContact(chatId: string, body: ContactUpdateBody) {
const jid = toJID(chatId);
let fullName = body.firstName;
if (body.lastName) {
fullName = `${body.firstName} ${body.lastName}`;
}
const action = {
fullName: fullName,
firstName: body.firstName,
saveOnPrimaryAddressbook: true,
};
await this.sock.addOrEditContact(jid, action);
const updates: Partial<Contact>[] = [
{
id: jid,
name: fullName,
},
];
this.sock.ev.emit('contacts.update', updates);
}
async getContact(query: ContactQuery) {
const jid = toJID(query.contactId);
const contact = await this.store.getContactById(jid);
@@ -1742,7 +1777,10 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
public async channelsList(query: ListChannelsQuery): Promise<Channel[]> {
const newsletters = await this.sock.newsletterSubscribed();
let channels = newsletters.map(toNewsletterMetadata).map(this.toChannel);
let channels = newsletters
.map(toNewsletterMetadata)
.filter(Boolean)
.map(this.toChannel);
if (query.role) {
// @ts-ignore
channels = channels.filter((channel) => channel.role === query.role);
@@ -1809,6 +1847,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
const messagesUpsert$ = fromEvent(this.sock.ev, 'messages.upsert').pipe(
map((event: BaileysEventMap['messages.upsert']) => event.messages),
mergeAll(),
filter((msg) => this.jids.include(msg.key.remoteJid)),
share(),
);
let [messagesFromMe$, messagesFromOthers$] = partition(
@@ -1819,6 +1858,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
mergeMap((msg) => this.processIncomingMessage(msg, true)),
share(), // share it so we don't process twice in message.any
);
messagesFromMe$ = messagesFromMe$.pipe(
filter((msg) => this.shouldProcessIncomingMessage(msg)),
);
messagesFromOthers$ = messagesFromOthers$.pipe(
mergeMap((msg) => this.processIncomingMessage(msg, true)),
share(), // share it so we don't process twice in message.any
@@ -1835,7 +1877,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
proto.Message.ProtocolMessage.Type.REVOKE,
),
mergeMap(async (message): Promise<WAMessageRevokedBody> => {
const afterMessage = await this.toWAMessage(message);
const afterMessage = this.toWAMessage(message);
// Extract the revoked message ID from protocolMessage.key
const revokedMessageId = message.message.protocolMessage.key?.id;
return {
@@ -1859,7 +1901,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
message.message?.protocolMessage?.editedMessage !== undefined,
),
mergeMap(async (message): Promise<WAMessageEditedBody> => {
const waMessage = await this.toWAMessage(message);
const waMessage = this.toWAMessage(message);
// Extract the body from editedMessage using extractBody function
const body =
extractBody(message.message.protocolMessage.editedMessage) || '';
@@ -1893,6 +1935,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
).pipe(
// @ts-ignore
mergeAll(),
filter((update) => this.jids.include(update.key.remoteJid)),
share(),
);
const messageAckDirect$ = messageUpdates$.pipe(
@@ -1904,6 +1947,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
fromEvent(this.sock.ev, 'message-receipt.update').pipe(
// @ts-ignore
mergeAll(),
filter((update) => this.jids.include(update.key.remoteJid)),
share(),
);
@@ -1971,6 +2015,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
this.events2.get(WAHAEvents.PRESENCE_UPDATE).switch(
fromEvent(this.sock.ev, 'presence.update').pipe(
filter((presence: any) => this.jids.include(presence.id)),
map((data: any) => this.toWahaPresences(data.id, data.presences)),
share(),
),
@@ -2001,7 +2046,13 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
//
// @ts-ignore
const calls$: Observable<WACallEvent[]> = fromEvent(this.sock.ev, 'call');
const call$ = calls$.pipe(mergeMap(identity), share());
const call$ = calls$.pipe(
mergeMap(identity),
filter((call: WACallEvent) =>
this.jids.include(call.groupJid || call.chatId),
),
share(),
);
this.events2.get(WAHAEvents.CALL_RECEIVED).switch(
call$.pipe(
filter((call: WACallEvent) => call.status === 'offer'),
@@ -2134,7 +2185,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return reaction;
}
protected async processIncomingMessage(message, downloadMedia = true) {
shouldProcessIncomingMessage(message): boolean {
// if there is no text or media message
if (!message) return;
if (!message.message) return;
@@ -2168,27 +2219,41 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
// Ignore key distribution messages
if (message?.message?.senderKeyDistributionMessage) return;
}
return true;
}
if (downloadMedia) {
try {
message = await this.downloadMedia(message);
} catch (e) {
this.logger.error('Failed when tried to download media for a message');
this.logger.error(e, e.stack);
}
protected async processIncomingMessage(
message,
downloadMedia: boolean,
): Promise<WAMessage | null> {
// Filter
if (!this.shouldProcessIncomingMessage(message)) {
return null;
}
// Convert
const wamessage = this.toWAMessageSafe(message);
if (!wamessage) {
return null;
}
// Media
if (downloadMedia) {
const media = await this.downloadMediaSafe(message);
wamessage.media = media;
}
return wamessage;
}
protected toWAMessageSafe(message): WAMessage | null {
try {
return await this.toWAMessage(message);
return this.toWAMessage(message);
} catch (error) {
this.logger.error('Failed to process incoming message');
this.logger.error(error);
console.trace(error);
return null;
}
}
protected toWAMessage(message): Promise<WAMessage> {
protected toWAMessage(message): WAMessage {
const fromToParticipant = getFromToParticipant(message.key);
const id = buildMessageId(message.key);
const body = extractBody(message.message);
@@ -2196,7 +2261,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
const ack = message.ack || StatusToAck(message.status);
const mediaContent = extractMediaContent(message.message);
const source = this.getMessageSource(message.key.id);
return Promise.resolve({
return {
id: id,
timestamp: ensureNumber(message.messageTimestamp),
from: toCusFormat(fromToParticipant.from),
@@ -2207,7 +2272,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
participant: toCusFormat(fromToParticipant.participant),
// Media
hasMedia: Boolean(mediaContent),
media: message.media || null,
media: null,
mediaUrl: message.media?.url,
// @ts-ignore
ack: ack,
@@ -2217,7 +2282,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
vCards: message.vCards,
replyTo: replyTo,
_data: message,
});
};
}
protected extractReplyTo(message): ReplyToMessage | null {
@@ -2409,7 +2474,17 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return { id: chatId, presences: presences };
}
protected downloadMedia(message) {
protected async downloadMediaSafe(message): Promise<WAMedia | null> {
try {
return await this.downloadMedia(message);
} catch (e) {
this.logger.error('Failed when tried to download media for a message');
this.logger.error(e, e.stack);
}
return null;
}
protected async downloadMedia(message): Promise<WAMedia | null> {
const processor = new NOWEBEngineMediaProcessor(this, this.loggerBuilder);
return this.mediaManager.processMedia(processor, message, this.name);
}
@@ -2673,5 +2748,20 @@ export function extractBody(message): string | null {
if (!body) {
body = content.buttonsResponseMessage?.selectedDisplayText;
}
// List message
if (!body) {
const type = getContentType(content);
if (type == 'listMessage') {
const list = content.listMessage;
const parts = [list.title, list.description, list.footerText];
body = parts.filter(Boolean).join('\n');
} else if (type === 'listResponseMessage') {
const response = content.listResponseMessage;
const parts = [response.title, response.description];
body = parts.filter(Boolean).join('\n');
}
}
return body;
}
@@ -14,6 +14,7 @@ import makeWASocket, {
updateMessageWithReceipt,
WAMessage,
} from '@adiwajshing/baileys';
import { WACallEvent } from '@adiwajshing/baileys/lib/Types/Call';
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import {
@@ -24,6 +25,7 @@ import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { JidFilter } from '@waha/core/utils/jids';
import {
GetChatMessagesFilter,
OverviewFilter,
@@ -39,6 +41,7 @@ import { waitUntil } from '@waha/utils/promiseTimeout';
import * as lodash from 'lodash';
import { toNumber } from 'lodash';
import { Logger } from 'pino';
import { filter } from 'rxjs';
import { IChatRepository } from './IChatRepository';
import { IContactRepository } from './IContactRepository';
@@ -82,6 +85,7 @@ export class NowebPersistentStore implements INowebStore {
constructor(
private logger: Logger,
public storage: INowebStorage,
private jids: JidFilter,
) {
this.socket = null;
this.chatRepo = storage.getChatRepository();
@@ -247,6 +251,7 @@ export class NowebPersistentStore implements INowebStore {
private async syncMessagesHistory(messages) {
const realMessages = messages.filter(isRealMessage);
messages = messages.filter((msg) => this.jids.include(msg.key.remoteJid));
await this.messagesRepo.upsert(realMessages);
this.logger.info(
`history sync - '${messages.length}' got messages, '${realMessages.length}' real messages`,
@@ -254,11 +259,13 @@ export class NowebPersistentStore implements INowebStore {
}
private async onMessagesUpsert(update) {
const { messages, type } = update;
const type = update.type;
if (type !== 'notify' && type !== 'append') {
this.logger.debug(`unexpected type for messages.upsert: '${type}'`);
return;
}
let messages = update.messages;
messages = messages.filter((msg) => this.jids.include(msg.key.remoteJid));
const realMessages = messages.filter(isRealMessage);
await this.messagesRepo.upsert(realMessages);
this.logger.debug(
@@ -270,6 +277,9 @@ export class NowebPersistentStore implements INowebStore {
for (const update of updates) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const jid = jidNormalizedUser(update.key.remoteJid!);
if (!this.jids.include(jid)) {
continue;
}
if (!update.key.id) {
continue;
}
@@ -333,12 +343,16 @@ export class NowebPersistentStore implements INowebStore {
delete chat['messages'];
chat.conversationTimestamp = toNumber(chat.conversationTimestamp) || null;
}
chats = chats.filter((chat) => this.jids.include(chat.id));
await this.chatRepo.upsertMany(chats);
this.logger.info(`store sync - '${chats.length}' synced chats`);
}
private async onGroupUpsert(groups: GroupMetadata[]) {
for (const group of groups) {
if (!this.jids.include(group.id)) {
continue;
}
await this.groupRepo.save(group);
}
this.logger.info(`store sync - '${groups.length}' synced groups`);
@@ -346,6 +360,9 @@ export class NowebPersistentStore implements INowebStore {
private async onGroupUpdate(groups: Partial<GroupMetadata>[]) {
for (const update of groups) {
if (!this.jids.include(update.id)) {
continue;
}
let group = await this.groupRepo.getById(update.id);
group = Object.assign(group || {}, update) as GroupMetadata;
await this.groupRepo.save(group);
@@ -356,6 +373,9 @@ export class NowebPersistentStore implements INowebStore {
private async onGroupParticipantsUpdate(data) {
const id: string = data.id;
if (!this.jids.include(id)) {
return;
}
const participants: string[] = data.participants;
const action: ParticipantAction = data.action;
@@ -419,6 +439,9 @@ export class NowebPersistentStore implements INowebStore {
private async onChatUpdate(updates: ChatUpdate[]) {
for (const update of updates) {
if (!this.jids.include(update.id)) {
continue;
}
const chat = (await this.chatRepo.getById(update.id)) || ({} as Chat);
Object.assign(chat, update);
chat.conversationTimestamp = toNumber(chat.conversationTimestamp) || null;
@@ -447,6 +470,9 @@ export class NowebPersistentStore implements INowebStore {
const ids = contacts.map((c) => c.id);
const contactById = await this.contactRepo.getEntitiesByIds(ids);
for (const update of contacts) {
if (!this.jids.include(update.id)) {
continue;
}
const contact = contactById.get(update.id) || {};
// remove undefined from data
Object.keys(update).forEach(
@@ -460,6 +486,9 @@ export class NowebPersistentStore implements INowebStore {
private async onContactUpdate(updates: Partial<Contact>[]) {
for (const update of updates) {
if (!this.jids.include(update.id)) {
continue;
}
let contact = await this.contactRepo.getById(update.id);
if (!contact) {
@@ -493,6 +522,9 @@ export class NowebPersistentStore implements INowebStore {
private async onMessageReaction(reactions) {
for (const { key, reaction } of reactions) {
if (!this.jids.include(key.remoteJid)) {
continue;
}
const msg = await this.messagesRepo.getByJidById(key.remoteJid, key.id);
if (!msg) {
this.logger.warn(
@@ -509,6 +541,9 @@ export class NowebPersistentStore implements INowebStore {
private async onMessageReceiptUpdate(updates) {
for (const { key, receipt } of updates) {
if (!this.jids.include(key.remoteJid)) {
continue;
}
const msg = await this.messagesRepo.getByJidById(key.remoteJid, key.id);
if (!msg) {
this.logger.warn(
@@ -544,6 +579,9 @@ export class NowebPersistentStore implements INowebStore {
}
private async onPresenceUpdate({ id, presences: update }) {
if (!this.jids.include(id)) {
return;
}
this.presences[id] = this.presences[id] || {};
Object.assign(this.presences[id], update);
}
@@ -446,7 +446,7 @@ export default (config: BaileysInMemoryStoreConfig) => {
) => {
const list = assertMessageList(jid);
const mode = !cursor || 'before' in cursor ? 'before' : 'after';
const cursorKey = !!cursor
const cursorKey = cursor
? 'before' in cursor
? cursor.before
: cursor.after
@@ -22,7 +22,7 @@ export class Sqlite3Storage extends INowebStorage {
constructor(filePath: string) {
super();
this.knex = Knex({
client: 'better-sqlite3',
client: 'sqlite3',
connection: { filename: filePath },
useNullAsDefault: true,
});
+30 -1
View File
@@ -30,7 +30,10 @@ export class WebjsClientCore extends Client {
public events = new EventEmitter();
private wpage: WPage = null;
constructor(options) {
constructor(
options,
protected tags: boolean,
) {
super(options);
// Wait until it's READY and inject more utils
this.on(Events.READY, async () => {
@@ -86,6 +89,32 @@ export class WebjsClientCore extends Client {
return;
},
);
if (this.tags) {
await this.attachTagsEvents();
}
}
async attachTagsEvents() {
await this.pupPage.evaluate(() => {
// @ts-ignore
if (window.decodeStanzaBack) {
return;
}
const tags = ['receipt', 'presence', 'chatstate'];
// @ts-ignore
window.decodeStanzaBack = window.Store.SocketWap.decodeStanza;
// @ts-ignore
window.Store.SocketWap.decodeStanza = async (...args) => {
// @ts-ignore
const result = await window.decodeStanzaBack(...args);
if (tags.includes(result?.tag)) {
// @ts-ignore
setTimeout(() => window.onTag(result), 0);
}
return result;
};
});
}
async destroy() {
+3 -3
View File
@@ -49,7 +49,7 @@ export function TagReceiptNodeToReceiptEvent(
const { attrs, content } = node;
const status = getStatusFromReceiptType(attrs.type);
if (status == null) {
return null;
return [];
}
const from = jidNormalizedUser(jid(attrs.from));
@@ -118,14 +118,14 @@ function handleGroupedReceipts(
fromMe: boolean,
isLid: boolean,
me: Me,
): ReceiptEvent[] | null {
): ReceiptEvent[] {
const { content } = node;
if (!Array.isArray(content)) {
return [];
}
const participantsTags = content.filter((c) => c.tag === 'participants');
if (participantsTags.length === 0) {
return null;
return [];
}
const receiptEvents: ReceiptEvent[] = [];
+63 -14
View File
@@ -80,7 +80,11 @@ import {
SendSeenRequest,
WANumberExistResult,
} from '@waha/structures/chatting.dto';
import { ContactQuery, ContactRequest } from '@waha/structures/contacts.dto';
import {
ContactQuery,
ContactRequest,
ContactUpdateBody,
} from '@waha/structures/contacts.dto';
import {
ACK_UNKNOWN,
SECOND,
@@ -99,6 +103,7 @@ import {
} from '@waha/structures/groups.dto';
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import { WAMedia } from '@waha/structures/media.dto';
import { ReplyToMessage } from '@waha/structures/message.dto';
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
import {
@@ -231,7 +236,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
rmMaxRetries: undefined,
});
this.addProxyConfig(clientOptions);
return new WebjsClientCore(clientOptions);
return new WebjsClientCore(clientOptions, this.getWebjsTagsFlag());
}
protected getWebjsTagsFlag() {
// Emit 'tag:*' events only when explicitly enabled in session config.
// This flag is required for presence.update and message.ack events.
// Disabled by default for performance and stability reasons.
return !!this.sessionConfig?.webjs?.tagsEventsOn;
}
private restartClient() {
@@ -779,7 +791,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
chat.id._serialized,
false,
);
const lastMessage = !!chat.lastMessage
const lastMessage = chat.lastMessage
? this.toWAMessage(chat.lastMessage)
: null;
return {
@@ -953,6 +965,16 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
/**
* Contacts methods
*/
public async upsertContact(chatId: string, body: ContactUpdateBody) {
const phoneNumber = chatId.split('@')[0];
await this.whatsapp.saveOrEditAddressbookContact(
phoneNumber,
body.firstName,
body.lastName,
true,
);
}
getContact(query: ContactQuery) {
return this.whatsapp
.getContactById(this.ensureSuffix(query.contactId))
@@ -1425,6 +1447,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
//
const messageReceived$ = fromEvent(this.whatsapp, Events.MESSAGE_RECEIVED);
const messagesFromOthers$ = messageReceived$.pipe(
filter((msg: Message) => this.jids.include(msg?.id?.remote)),
mergeMap((msg: any) => this.processIncomingMessage(msg, true)),
share(),
);
@@ -1432,6 +1455,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
const messageCreate$ = fromEvent(this.whatsapp, Events.MESSAGE_CREATE);
const messagesFromAll$ = messageCreate$.pipe(
filter((msg: Message) => this.jids.include(msg?.id?.remote)),
mergeMap((msg: any) => this.processIncomingMessage(msg, true)),
share(),
);
@@ -1442,6 +1466,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
Events.MESSAGE_CIPHERTEXT,
);
const messagesWaiting$ = messageCiphertext$.pipe(
filter((msg: Message) => this.jids.include(msg?.id?.remote)),
mergeMap((msg: any) => this.processIncomingMessage(msg, false)),
share(),
);
@@ -1455,6 +1480,9 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
},
);
const messagesRevoked$ = messageRevoked$.pipe(
filter((evt: any) =>
this.jids.include(evt?.after?.id?.remote || evt?.before?.id?.remote),
),
map((event): WAMessageRevokedBody => {
const afterMessage = event.after ? this.toWAMessage(event.after) : null;
const beforeMessage = event.before
@@ -1473,6 +1501,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
const messageReaction$ = fromEvent(this.whatsapp, 'message_reaction');
const messagesReaction$ = messageReaction$.pipe(
filter((reaction: Reaction) => this.jids.include(reaction?.id?.remote)),
map(this.processMessageReaction.bind(this)),
filter(Boolean),
);
@@ -1486,6 +1515,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
},
);
const messagesEdit$ = messageEdit$.pipe(
filter((event: any) => this.jids.include(event?.message?.id?.remote)),
map((event): WAMessageEditedBody => {
const message = this.toWAMessage(event.message);
return {
@@ -1509,6 +1539,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
map((event) => event.message),
map<any, WAMessage>(this.toWAMessage.bind(this)),
filter((ack) => !isJidGroup(ack.to) && !isJidStatusBroadcast(ack.to)),
filter((ack) => this.jids.include(ack.to)),
);
const tagReceiptNode$ = fromEvent(this.whatsapp, Events.TAG_RECEIPT);
const messageAckGroups$ = tagReceiptNode$.pipe(
@@ -1518,6 +1549,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
filter(Boolean),
mergeMap(this.TagReceiptToMessageAck.bind(this)),
filter((ack) => isJidGroup(ack.to) || isJidStatusBroadcast(ack.to)),
filter((ack) => this.jids.include(ack.to)),
);
const messageAckAll$ = merge(messagesAckDM$, messageAckGroups$);
@@ -1538,11 +1570,13 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
const presences$ = tagPresenceNode$.pipe(
map(TagPresenceToPresence),
filter(Boolean),
filter((presence: any) => this.jids.include(presence.id)),
);
const tagChatstateNode$ = fromEvent(this.whatsapp, 'tag:chatstate');
const chatstatePresences$ = tagChatstateNode$.pipe(
map(TagChatstateToPresence),
filter(Boolean),
filter((presence: any) => this.jids.include(presence.id)),
);
const presenceUpdate$ = merge(presences$, chatstatePresences$);
this.events2.get(WAHAEvents.PRESENCE_UPDATE).switch(presenceUpdate$);
@@ -1611,6 +1645,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
},
);
const chatsArchived$ = chatArchived$.pipe(
filter((event: any) => this.jids.include(event?.chat?.id?._serialized)),
map((event) => {
return {
id: event.chat.id._serialized,
@@ -1626,6 +1661,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
//
const call$ = fromEvent(this.whatsapp, 'call');
const calls$ = call$.pipe(
filter((call: Call) => this.jids.include((call as any)?.from)),
map((call: Call) => {
return {
id: call.id,
@@ -1643,15 +1679,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
message: Message,
downloadMedia = true,
) {
// Convert
const wamessage = this.toWAMessage(message);
// Media
if (downloadMedia) {
try {
message = await this.downloadMedia(message);
} catch (e) {
this.logger.error('Failed when tried to download media for a message');
this.logger.error(e, e.stack);
}
const media = await this.downloadMediaSafe(message);
wamessage.media = media;
}
return this.toWAMessage(message);
return wamessage;
}
private processMessageReaction(reaction: Reaction): WAMessageReaction {
@@ -1723,8 +1758,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
// Media
// @ts-ignore
hasMedia: Boolean(message.hasMedia),
// @ts-ignore
media: message.media || null,
media: null,
// @ts-ignore
mediaUrl: message.media?.url,
// @ts-ignore
@@ -1767,9 +1801,24 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return contact;
}
protected downloadMedia(message: Message) {
protected async downloadMediaSafe(message): Promise<WAMedia | null> {
try {
return await this.downloadMedia(message);
} catch (e) {
this.logger.error('Failed when tried to download media for a message');
this.logger.error(e, e.stack);
}
return null;
}
protected async downloadMedia(message: Message) {
const processor = new WEBJSEngineMediaProcessor();
return this.mediaManager.processMedia(processor, message, this.name);
const media = await this.mediaManager.processMedia(
processor,
message,
this.name,
);
return media;
}
protected getMessageOptions(request: any): any {
+5 -2
View File
@@ -2,12 +2,15 @@ import {
NotImplementedException,
UnprocessableEntityException,
} from '@nestjs/common';
import { getEngineName } from '@waha/config';
export const DOCS_URL = 'https://waha.devlike.pro/';
const engine = getEngineName();
export class NotImplementedByEngineError extends NotImplementedException {
constructor(msg = '') {
let error = 'The method is not implemented by the engine.';
let error = `The method is not implemented by '${engine}' engine. Check the docs and try another engine: ${DOCS_URL}`;
if (msg) {
error = `${msg} ${error}`;
}
@@ -18,7 +21,7 @@ export class NotImplementedByEngineError extends NotImplementedException {
export class AvailableInPlusVersion extends UnprocessableEntityException {
constructor(feature: string = 'The feature') {
super(
`${feature} is available only in Plus version. Check this out: ${DOCS_URL}`,
`${feature} is available only in Plus version for '${engine}' engine. Check this out: ${DOCS_URL}`,
);
}
}
+1
View File
@@ -200,6 +200,7 @@ export class SessionManagerCore extends SessionManager implements OnModuleInit {
sessionStore: this.store,
proxyConfig: proxyConfig,
sessionConfig: this.sessionConfig,
ignore: this.ignoreChatsConfig(this.sessionConfig),
};
if (this.EngineClass === WhatsappSessionWebJSCore) {
sessionConfig.engineConfig = this.webjsEngineConfigService.getConfig();
+2 -1
View File
@@ -1,4 +1,5 @@
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
import { WAMedia } from '@waha/structures/media.dto';
/**
* General interface for MediaManager - one that handles the logic
@@ -9,7 +10,7 @@ interface IMediaManager {
processor: IMediaEngineProcessor<Message>,
message: Message,
session: string,
): Promise<Message>;
): Promise<WAMedia | null>;
close(): void;
}
+2 -4
View File
@@ -102,7 +102,7 @@ export class MediaManager implements IMediaManager {
processor: IMediaEngineProcessor<Message>,
message: Message,
session: string,
): Promise<Message> {
): Promise<WAMedia | null> {
let messageId: string;
try {
messageId = processor.getMessageId(message);
@@ -134,9 +134,7 @@ export class MediaManager implements IMediaManager {
// @ts-ignore
media.error.details = `${err.stack}`;
}
// @ts-ignore
message.media = media;
return message;
return media;
}
private async fetchMedia(
+1 -1
View File
@@ -3,7 +3,7 @@ import { SessionConfig } from '../../structures/sessions.dto';
export abstract class ISessionConfigRepository {
abstract saveConfig(
sessionName: string,
config: SessionConfig | null,
config: SessionConfig,
): Promise<void>;
abstract getConfig(sessionName: string): Promise<SessionConfig | null>;
@@ -48,13 +48,13 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
return JSON.parse(content);
}
async saveConfig(sessionName: string, config: SessionConfig | null) {
// Create folder if not exist
async saveConfig(sessionName: string, config: SessionConfig) {
// Create a folder if not exist
const folder = this.store.getSessionDirectory(sessionName);
await fs.mkdir(folder, { recursive: true });
// Save config
const filepath = this.getFilePath(sessionName);
const content = JSON.stringify(config || null);
const content = JSON.stringify(config || {});
await writeFileAtomic(filepath, content);
}
+1 -1
View File
@@ -68,7 +68,7 @@ export class LocalStoreCore extends LocalStore {
const engineDir = this.getEngineDirectory();
const database = path.join(engineDir, 'waha.sqlite3');
return Knex({
client: 'better-sqlite3',
client: 'sqlite3',
connection: { filename: database },
useNullAsDefault: true,
});
+27 -2
View File
@@ -1,11 +1,15 @@
import { isJidGroup, isJidMetaIa } from '@adiwajshing/baileys';
import {
isJidGroup,
isJidMetaIa,
isJidStatusBroadcast,
} from '@adiwajshing/baileys';
import {
isJidBroadcast,
isLidUser,
} from '@adiwajshing/baileys/lib/WABinary/jid-utils';
export function isJidNewsletter(jid: string) {
return jid.endsWith('@newsletter');
return jid?.endsWith('@newsletter');
}
export function isJidCus(jid: string) {
@@ -35,3 +39,24 @@ export function toJID(chatId) {
const number = chatId.split('@')[0];
return number + '@s.whatsapp.net';
}
export interface IgnoreJidConfig {
status: boolean;
groups: boolean;
channels: boolean;
}
export class JidFilter {
constructor(public ignore: IgnoreJidConfig) {}
include(jid: string): boolean {
if (this.ignore.status && isJidStatusBroadcast(jid)) {
return false;
} else if (this.ignore.groups && isJidGroup(jid)) {
return false;
} else if (this.ignore.channels && isJidNewsletter(jid)) {
return false;
}
return true;
}
}
+6 -1
View File
@@ -18,7 +18,12 @@ export function serializeError(err: unknown) {
const properties = Object.getOwnPropertyNames(err);
// getOwnPropertyNames does not get 'name', like "ReferenceError"
properties.push('name');
return JSON.parse(JSON.stringify(err, properties));
const result = JSON.parse(JSON.stringify(err, properties));
// Put the "stack" field at the end, too wordy
const stack = result.stack;
delete result.stack;
result.stack = stack;
return result;
}
@Catch()
+33
View File
@@ -8,8 +8,10 @@ import { IsFileType } from '@waha/nestjs/validation/IsFileType';
import { GetChatMessagesQuery } from '@waha/structures/chats.dto';
import { plainToInstance, Transform, Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
IsUrl,
@@ -456,3 +458,34 @@ export class NewMessageIDResponse {
})
id: string;
}
export class MessagePollVoteRequest extends ChatRequest {
@ApiProperty({
description:
'The ID of the poll message. Format: {fromMe}_{chatID}_{messageId}[_{participant}] or just ID for GOWS',
example: 'false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
required: true,
})
@IsString()
@IsNotEmpty()
pollMessageId: string;
@ApiProperty({
description:
'Only for Channels - server message id (if known); if omitted, API may look it up in the storage',
required: false,
example: null,
})
@IsOptional()
@IsNumber()
pollServerId?: number;
@ApiProperty({
description: 'Poll options you are voting for',
example: 'Awesome!',
isArray: true,
})
@IsArray()
@IsString({ each: true })
votes: string[];
}
+141
View File
@@ -0,0 +1,141 @@
import { ApiExtraModels, ApiProperty } from '@nestjs/swagger';
import { ChatRequest } from '@waha/structures/chatting.dto';
import { ChatIdProperty } from '@waha/structures/properties.dto';
import { Type } from 'class-transformer';
import {
ArrayMinSize,
IsArray,
IsNotEmpty,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
class Row {
@ApiProperty({ example: 'Option 1' })
@IsString()
@IsNotEmpty()
title: string;
@ApiProperty({ example: 'Description of option 1', required: false })
@IsOptional()
@IsString()
description?: string;
@ApiProperty({ example: 'option1' })
@IsString()
@IsNotEmpty()
rowId: string;
}
class Section {
@ApiProperty({ example: 'Menu' })
@IsString()
@IsNotEmpty()
title: string;
@ValidateNested({ each: true })
@Type(() => Row)
@IsArray()
@ArrayMinSize(1)
@ApiProperty({
example: [
{ title: 'Option 1', rowId: 'option1', description: 'First option' },
{ title: 'Option 2', rowId: 'option2', description: 'Second option' },
],
})
rows: Row[];
}
export class SendListMessage {
@ApiProperty({ example: 'Example List' })
@IsString()
@IsNotEmpty()
title: string;
@ApiProperty({ example: 'Choose one of the options', required: false })
@IsOptional()
@IsString()
description?: string;
@ApiProperty({ example: 'Footer note', required: false })
@IsOptional()
@IsString()
footer?: string;
@ApiProperty({ example: 'Select' })
@IsString()
@IsNotEmpty()
button: string;
@ValidateNested({ each: true })
@Type(() => Section)
@IsArray()
@ArrayMinSize(1)
@ApiProperty({
example: [
{
title: 'Section 1',
rows: [
{ title: 'Option 1', rowId: 'option1', description: 'Description 1' },
{ title: 'Option 2', rowId: 'option2', description: 'Description 2' },
],
},
],
})
sections: Section[];
}
@ApiExtraModels(SendListMessage)
export class SendListRequest extends ChatRequest {
@ChatIdProperty()
@IsString()
chatId: string;
@ValidateNested()
@Type(() => SendListMessage)
@ApiProperty({
type: SendListMessage,
example: {
title: 'Simple Menu',
description: 'Please choose an option',
footer: 'Thank you!',
button: 'Choose',
sections: [
{
title: 'Main',
rows: [
{
title: 'Option 1',
rowId: 'option1',
description: null,
},
{
title: 'Option 2',
rowId: 'option2',
description: null,
},
{
title: 'Option 3',
rowId: 'option3',
description: null,
},
],
},
],
},
})
message: SendListMessage;
@ApiProperty({
description:
'The ID of the message to reply to - false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
example: null,
required: false,
})
@IsOptional()
@IsString()
reply_to?: string;
}
export { Row, Section };
+18
View File
@@ -54,3 +54,21 @@ export class ContactRequest extends SessionBaseRequest {
@IsString()
contactId: string;
}
export class ContactUpdateBody {
@ApiProperty({
description: 'Contact First Name',
example: 'John',
required: true,
})
@IsString()
firstName: string = 'John';
@ApiProperty({
description: 'Contact Last Name',
example: 'Doe',
required: true,
})
@IsString()
lastName: string = 'Doe';
}
+69 -1
View File
@@ -7,6 +7,7 @@ import {
IsBoolean,
IsOptional,
IsString,
Matches,
MaxLength,
ValidateNested,
} from 'class-validator';
@@ -88,6 +89,42 @@ export class NowebConfig {
markOnline: boolean = true;
}
export class WebjsConfig {
@ApiProperty({
description:
"Enable emission of special 'tag:*' engine events required for presence.update and message.ack.\n" +
'WARNING: Enabling this may have performance and stability impact. Disabled by default.',
required: false,
default: false,
})
@IsBoolean()
@IsOptional()
tagsEventsOn?: boolean = false;
}
export class IgnoreConfig {
@ApiProperty({
description: 'Ignore a status@broadcast (stories) events',
})
@IsBoolean()
@IsOptional()
status?: boolean;
@ApiProperty({
description: 'Ignore groups events',
})
@IsBoolean()
@IsOptional()
groups?: boolean;
@ApiProperty({
description: 'Ignore channels events',
})
@IsBoolean()
@IsOptional()
channels?: boolean;
}
export class SessionConfig {
@ValidateNested({ each: true })
@Type(() => WebhookConfig)
@@ -122,7 +159,20 @@ export class SessionConfig {
})
@IsBoolean()
@IsOptional()
debug: boolean;
debug?: boolean;
@ApiProperty({
example: {
status: null,
groups: null,
channels: null,
},
description: 'Ignore some events related to specific chats',
})
@ValidateNested()
@Type(() => IgnoreConfig)
@IsOptional()
ignore?: IgnoreConfig;
@ApiProperty({
example: {
@@ -136,6 +186,15 @@ export class SessionConfig {
@Type(() => NowebConfig)
@IsOptional()
noweb?: NowebConfig;
@ApiProperty({
description: 'WebJS-specific settings.',
required: false,
})
@ValidateNested()
@Type(() => WebjsConfig)
@IsOptional()
webjs?: WebjsConfig;
}
export class SessionDTO {
@@ -154,6 +213,11 @@ export class MeInfo {
@ChatIdProperty()
id: string;
@ApiProperty({
example: '123123@lid',
})
lid?: string;
pushName: string;
}
@@ -179,6 +243,10 @@ export class SessionCreateRequest {
@IsString()
@IsOptional()
@MaxLength(DB_NAME_LIMIT - DB_NAME_MAX_PREFIX_LEN)
@Matches(/^[a-zA-Z0-9_-]*$/, {
message:
'Session name can only contain alphanumeric characters, hyphens, and underscores (a-z, A-Z, 0-9, -, _) or be empty',
})
name: string | undefined;
@ValidateNested()
+5 -6
View File
@@ -1,4 +1,6 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
import { getEngineName } from '@waha/config';
import { getBrowserExecutablePath } from './core/abc/session.abc';
import { WAHAEngine } from './structures/enums.dto';
import { WAHAEnvironment } from './structures/environment.dto';
@@ -26,14 +28,9 @@ export function getWAHAVersion(): WAHAVersion {
return WAHAVersion.CORE;
}
export function getEngineName(): string {
// Load engine name from WHATSAPP_DEFAULT_ENGINE environment variable
// If not set - use WEBJS
return process.env.WHATSAPP_DEFAULT_ENGINE || WAHAEngine.WEBJS;
}
export const VERSION: WAHAEnvironment = {
version: '2025.7.7',
version: '2025.8.3',
engine: getEngineName(),
tier: getWAHAVersion(),
browser:
@@ -41,3 +38,5 @@ export const VERSION: WAHAEnvironment = {
};
export const IsChrome = VERSION.browser?.includes('chrome');
export { getEngineName };
+2 -2
View File
@@ -2,11 +2,11 @@
"waha": {
"gows": {
"repo": "devlikeapro/gows",
"ref": "v1.0.9"
"ref": "v1.0.13"
},
"dashboard": {
"repo": "devlikeapro/dashboard",
"ref": "a74c4ede4e963dd7f4a0eebb9923e3e174ef51fd"
"ref": "a29267de044e439535f99bff42568aec5df7b462"
}
}
}
+515 -1528
View File
File diff suppressed because it is too large. Load diff