Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79233e09e3 | ||
|
|
a96a4b2161 | ||
|
|
f736105873 | ||
|
|
2c1ea2c7c1 | ||
|
|
216e00cc25 | ||
|
|
ac1a015046 | ||
|
|
8f4af6bb08 | ||
|
|
acee1b7d79 | ||
|
|
99241c3089 | ||
|
|
8e764372d2 | ||
|
|
fa21a60cf3 | ||
|
|
02fa0ea06b | ||
|
|
8d1ad04625 | ||
|
|
a639baad8d | ||
|
|
2e24107018 | ||
|
|
750ce208b3 | ||
|
|
057e0a0e70 | ||
|
|
255f84a12d | ||
|
|
ac6d14394a | ||
|
|
cb77c2fc49 | ||
|
|
5c279c5240 | ||
|
|
7e719d8894 | ||
|
|
8faa3ca5a4 | ||
|
|
5dd8bf140d | ||
|
|
0501c37500 | ||
|
|
1496274a99 | ||
|
|
b6ba3951da | ||
|
|
a4a3dc93ad | ||
|
|
57eb0e14ca | ||
|
|
d8970326be | ||
|
|
06412c2c1e | ||
|
|
0b39645c50 | ||
|
|
76f8cc6f53 | ||
|
|
ea79b1d886 | ||
|
|
e54604eb97 | ||
|
|
d267a29e87 | ||
|
|
d4625359e6 | ||
|
|
3618b92c3e | ||
|
|
982092cf2b | ||
|
|
6a452cd66e | ||
|
|
19625e38e9 | ||
|
|
208f4f3d78 | ||
|
|
a9ff7d763d | ||
|
|
e290fd545f | ||
|
|
84f111e2c1 | ||
|
|
55dddd5991 | ||
|
|
638555297c | ||
|
|
0e4de03da3 | ||
|
|
c6219be356 | ||
|
|
2460a23cab |
No files matched your search
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(yarn build:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(node:*)",
|
||||
"Bash(npm show:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(yarn test:unit:*)",
|
||||
"Bash(npx tsc:*)",
|
||||
"Bash(pre-commit run:*)",
|
||||
"Bash(yarn test:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
# run: yarn test:unit
|
||||
|
||||
docker-build:
|
||||
if: github.repository == 'devlikeapro/waha-plus'
|
||||
if: github.repository == 'devlikeapro/waha'
|
||||
# needs: unit-tests
|
||||
runs-on: ${{ matrix.runner }}
|
||||
name:
|
||||
@@ -73,6 +73,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: dev
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check recent changes
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
approvedGitRepositories:
|
||||
- '**'
|
||||
|
||||
compressionLevel: mixed
|
||||
|
||||
enableGlobalCache: false
|
||||
|
||||
enableScripts: true
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
npmMinimalAgeGate: 0
|
||||
|
||||
supportedArchitectures:
|
||||
cpu:
|
||||
- arm
|
||||
|
||||
+2
-2
@@ -16,10 +16,10 @@ RUN apt-get update && \
|
||||
WORKDIR /git
|
||||
COPY package.json .
|
||||
COPY yarn.lock .
|
||||
COPY .yarnrc.yml .
|
||||
ENV YARN_CHECKSUM_BEHAVIOR=update
|
||||
|
||||
RUN npm install -g corepack && corepack enable
|
||||
RUN yarn set version 4.9.2
|
||||
RUN yarn install
|
||||
|
||||
# App
|
||||
@@ -179,7 +179,7 @@ RUN if [ "$USE_BROWSER" = "chromium" ]; then \
|
||||
# Install Chrome
|
||||
# Available versions:
|
||||
# https://www.ubuntuupdates.org/package/google_chrome/stable/main/base/google-chrome-stable
|
||||
ARG CHROME_VERSION="140.0.7339.80-1"
|
||||
ARG CHROME_VERSION="140.0.7339.207-1"
|
||||
ARG OPUSTAGS_VERSION="1.10.1"
|
||||
RUN if [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
wget --no-verbose -O /tmp/chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb \
|
||||
|
||||
@@ -41,11 +41,11 @@ up-noweb-libsignal:
|
||||
yarn up libsignal@github:devlikeapro/libsignal-node#fork-master
|
||||
|
||||
up-webjs:
|
||||
yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2026-02-18
|
||||
yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2026-06-26
|
||||
|
||||
up-wpp:
|
||||
yarn up @wppconnect-team/wppconnect
|
||||
yarn up @wppconnect/wa-js
|
||||
yarn up @wppconnect-team/wppconnect@github:wppconnect-team/wppconnect#master
|
||||
yarn up @wppconnect/wa-js@github:wppconnect-team/wa-js#main
|
||||
|
||||
up-rust-bridge:
|
||||
yarn up -R whatsapp-rust-bridge
|
||||
@@ -61,6 +61,17 @@ gows:
|
||||
(export PATH=${HOME}/go/bin:${PATH} || echo failed) && \
|
||||
make all
|
||||
|
||||
ORIGIN ?= waha-plus
|
||||
CORE_REMOTE ?= waha
|
||||
|
||||
release:
|
||||
node scripts/release.js
|
||||
|
||||
release-push: release
|
||||
git push $(ORIGIN) core plus
|
||||
git push --force-with-lease $(ORIGIN) dev
|
||||
git push $(CORE_REMOTE) core
|
||||
|
||||
up-dashboard:
|
||||
node scripts/up-dashboard.js
|
||||
|
||||
|
||||
+6
-6
@@ -5,12 +5,12 @@
|
||||
"plugins": ["@nestjs/swagger"],
|
||||
"watchPathIgnorePatterns": ["node_modules", "src/dashboard", "dist"],
|
||||
"assets": [
|
||||
"dashboard/**",
|
||||
"core/engines/webjs/*",
|
||||
"plus/engines/webjs/*",
|
||||
"apps/chatwoot/i18n/locales/*.yaml",
|
||||
"apps/chatwoot/i18n/locales/*.yml"
|
||||
{ "include": "dashboard/**", "watchAssets": false },
|
||||
{ "include": "core/engines/webjs/*", "watchAssets": true },
|
||||
{ "include": "plus/engines/webjs/*", "watchAssets": true },
|
||||
{ "include": "apps/chatwoot/i18n/locales/*.yaml", "watchAssets": true },
|
||||
{ "include": "apps/chatwoot/i18n/locales/*.yml", "watchAssets": true }
|
||||
],
|
||||
"watchAssets": true
|
||||
"watchAssets": false
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -60,8 +60,8 @@
|
||||
"@types/passport": "^1.0.17",
|
||||
"@types/sqlite3": "^5.1.0",
|
||||
"@types/ws": "^8.5.4",
|
||||
"@wppconnect-team/wppconnect": "^2.2.1",
|
||||
"@wppconnect/wa-js": "^4.3.0",
|
||||
"@wppconnect-team/wppconnect": "github:wppconnect-team/wppconnect#master",
|
||||
"@wppconnect/wa-js": "github:wppconnect-team/wa-js#main",
|
||||
"adm-zip": "0.5.10",
|
||||
"agentkeepalive": "^4.5.0",
|
||||
"async-lock": "^1.4.1",
|
||||
@@ -119,7 +119,7 @@
|
||||
"undici": "^7.16.0",
|
||||
"uniqid": "^5.4.0",
|
||||
"user-agents": "^1.1.669",
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2026-02-18",
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2026-06-26",
|
||||
"write-file-atomic": "^6.0.0",
|
||||
"yaml": "^2.7.1",
|
||||
"zod": "^4.3.6"
|
||||
@@ -205,5 +205,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"packageManager": "yarn@4.9.2"
|
||||
"packageManager": "yarn@4.17.1"
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { execFileSync } = require('child_process');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const readline = require('readline');
|
||||
|
||||
const DEV_BRANCH = process.env.WAHA_DEV_BRANCH || 'dev';
|
||||
const CORE_BRANCH = process.env.WAHA_CORE_BRANCH || 'core';
|
||||
const PLUS_BRANCH = process.env.WAHA_PLUS_BRANCH || 'plus';
|
||||
|
||||
const CORE_PREFIX = '[core]';
|
||||
const PLUS_PREFIX = '[PLUS]';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes('--dry-run');
|
||||
const assumeYes = args.includes('--yes') || args.includes('-y');
|
||||
|
||||
function git(gitArgs, options) {
|
||||
const opts = options || {};
|
||||
return execFileSync('git', gitArgs, {
|
||||
encoding: 'utf8',
|
||||
stdio: opts.inherit ? 'inherit' : ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
}
|
||||
|
||||
function gitOut(gitArgs) {
|
||||
return git(gitArgs, { inherit: false }).trim();
|
||||
}
|
||||
|
||||
function log(message) {
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
console.error(`\n✗ ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function ensureCleanTree() {
|
||||
const status = gitOut(['status', '--porcelain']);
|
||||
if (status) {
|
||||
fail(
|
||||
'Working tree is not clean. Commit or stash your changes before releasing.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureBranchExists(branch) {
|
||||
try {
|
||||
git(['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`]);
|
||||
} catch {
|
||||
fail(`Branch "${branch}" does not exist locally.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Non-merge commits in `boundary..head`, oldest first, whose subject starts
|
||||
// with prefix. The boundary is the last-released plus tip: dev is rebased onto
|
||||
// plus every release, so `plus..dev` is exactly the new, unreleased work.
|
||||
// We intentionally do not use `git cherry` (patch-id matching) because core is
|
||||
// a rewritten history whose old commits share no patch-ids with dev.
|
||||
function commitsToCherryPick(boundary, head, prefix) {
|
||||
const format = '%H%x09%s';
|
||||
const output = gitOut([
|
||||
'rev-list',
|
||||
'--reverse',
|
||||
'--no-merges',
|
||||
`--format=${format}`,
|
||||
`${boundary}..${head}`,
|
||||
]);
|
||||
if (!output) {
|
||||
return [];
|
||||
}
|
||||
const picks = [];
|
||||
for (const line of output.split('\n')) {
|
||||
// rev-list --format prefixes each entry with a "commit <sha>" header line.
|
||||
if (!line || line.startsWith('commit ')) {
|
||||
continue;
|
||||
}
|
||||
const tab = line.indexOf('\t');
|
||||
const sha = line.slice(0, tab);
|
||||
const subject = line.slice(tab + 1);
|
||||
if (subject.startsWith(prefix)) {
|
||||
picks.push({ sha: sha, subject: subject });
|
||||
}
|
||||
}
|
||||
return picks;
|
||||
}
|
||||
|
||||
function cherryPickAll(picks) {
|
||||
for (const pick of picks) {
|
||||
log(` cherry-pick ${pick.sha.slice(0, 9)} ${pick.subject}`);
|
||||
if (dryRun) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
git(['cherry-pick', pick.sha], { inherit: true });
|
||||
} catch {
|
||||
git(['cherry-pick', '--abort'], { inherit: true });
|
||||
fail(
|
||||
`Cherry-pick of ${pick.sha.slice(0, 9)} failed (conflict). ` +
|
||||
`Aborted the cherry-pick — resolve manually and re-run.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkout(branch) {
|
||||
log(`\n→ checkout ${branch}`);
|
||||
if (!dryRun) {
|
||||
git(['checkout', branch], { inherit: true });
|
||||
}
|
||||
}
|
||||
|
||||
function confirm(question) {
|
||||
if (assumeYes || dryRun) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
return new Promise(function resolver(resolve) {
|
||||
rl.question(`${question} [y/N] `, function onAnswer(answer) {
|
||||
rl.close();
|
||||
resolve(/^y(es)?$/i.test(answer.trim()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
ensureCleanTree();
|
||||
[DEV_BRANCH, CORE_BRANCH, PLUS_BRANCH].forEach(ensureBranchExists);
|
||||
|
||||
const startBranch = gitOut(['rev-parse', '--abbrev-ref', 'HEAD']);
|
||||
|
||||
if (dryRun) {
|
||||
log('Running in --dry-run mode: no branches will be modified.\n');
|
||||
}
|
||||
|
||||
// Capture the last-released plus tip before we touch anything. Step 3 merges
|
||||
// core into plus and moves the branch, so both pick sets must be computed
|
||||
// against this saved boundary, not the live plus ref.
|
||||
const boundary = gitOut(['rev-parse', PLUS_BRANCH]);
|
||||
log(`Boundary (last released ${PLUS_BRANCH}): ${boundary.slice(0, 9)}\n`);
|
||||
|
||||
const corePicks = commitsToCherryPick(boundary, DEV_BRANCH, CORE_PREFIX);
|
||||
log(
|
||||
`Found ${corePicks.length} "${CORE_PREFIX}" commit(s) in ` +
|
||||
`${DEV_BRANCH} since last ${PLUS_BRANCH} release.`,
|
||||
);
|
||||
|
||||
// Step 1 + 2: bring missing [core] commits onto core.
|
||||
checkout(CORE_BRANCH);
|
||||
cherryPickAll(corePicks);
|
||||
|
||||
// Step 3: merge the freshly updated core into plus.
|
||||
checkout(PLUS_BRANCH);
|
||||
log(`\n→ merge ${CORE_BRANCH} into ${PLUS_BRANCH}`);
|
||||
if (!dryRun) {
|
||||
try {
|
||||
git(['merge', '--no-edit', CORE_BRANCH], { inherit: true });
|
||||
} catch {
|
||||
git(['merge', '--abort'], { inherit: true });
|
||||
fail(
|
||||
`Merge of ${CORE_BRANCH} into ${PLUS_BRANCH} failed (conflict). ` +
|
||||
`Aborted the merge — resolve manually and re-run.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: bring missing [PLUS] commits onto plus (same saved boundary).
|
||||
const plusPicks = commitsToCherryPick(boundary, DEV_BRANCH, PLUS_PREFIX);
|
||||
log(
|
||||
`\nFound ${plusPicks.length} "${PLUS_PREFIX}" commit(s) in ` +
|
||||
`${DEV_BRANCH} since last ${PLUS_BRANCH} release.`,
|
||||
);
|
||||
cherryPickAll(plusPicks);
|
||||
|
||||
// Step 5: rebase dev onto the freshly built plus.
|
||||
log(`\n→ rebase ${DEV_BRANCH} onto ${PLUS_BRANCH} (rewrites ${DEV_BRANCH})`);
|
||||
const proceed = await confirm(
|
||||
`This will force-rewrite "${DEV_BRANCH}". Continue?`,
|
||||
);
|
||||
if (!proceed) {
|
||||
fail('Aborted before rebasing dev. core/plus changes are kept.');
|
||||
}
|
||||
checkout(DEV_BRANCH);
|
||||
if (!dryRun) {
|
||||
try {
|
||||
git(['rebase', PLUS_BRANCH], { inherit: true });
|
||||
} catch {
|
||||
git(['rebase', '--abort'], { inherit: true });
|
||||
fail(
|
||||
`Rebase of ${DEV_BRANCH} onto ${PLUS_BRANCH} failed (conflict). ` +
|
||||
`Aborted the rebase — resolve manually and re-run.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
checkout(startBranch);
|
||||
log('\nDry run complete. Re-run without --dry-run to apply.');
|
||||
} else {
|
||||
log(
|
||||
`\n✓ Release complete. ${DEV_BRANCH} now sits on top of ${PLUS_BRANCH}.`,
|
||||
);
|
||||
log(
|
||||
` Push when ready: git push origin ${CORE_BRANCH} ${PLUS_BRANCH} ` +
|
||||
`&& git push --force-with-lease origin ${DEV_BRANCH}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(function onError(error) {
|
||||
fail(error.message || String(error));
|
||||
});
|
||||
@@ -12,12 +12,16 @@ import {
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
import { CanServer } from '@waha/core/auth/policies';
|
||||
import { CanServer, CanSession, FromBody } from '@waha/core/auth/policies';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { ApiKeyService } from '@waha/core/services/ApiKeyService';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { ApiKeyDTO, ApiKeyRequest } from '@waha/structures/apikeys.dto';
|
||||
import {
|
||||
ApiKeyDTO,
|
||||
ApiKeyRequest,
|
||||
ScopedApiKeyRequest,
|
||||
} from '@waha/structures/apikeys.dto';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/keys')
|
||||
@@ -44,6 +48,26 @@ export class ApiKeysController {
|
||||
return this.service.list();
|
||||
}
|
||||
|
||||
@Post('/media')
|
||||
@ApiOperation({
|
||||
summary: 'Create or get a media-download-only API key for a session',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Read, FromBody('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async media(@Body() body: ScopedApiKeyRequest): Promise<ApiKeyDTO> {
|
||||
return this.service.createOrGetMediaKey(body.session);
|
||||
}
|
||||
|
||||
@Post('/control')
|
||||
@ApiOperation({
|
||||
summary: 'Create or get a control-only API key for a session',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Control, FromBody('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async control(@Body() body: ScopedApiKeyRequest): Promise<ApiKeyDTO> {
|
||||
return this.service.createOrGetControlKey(body.session);
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@ApiOperation({ summary: 'Update an API key' })
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
|
||||
@@ -7,7 +7,12 @@ import {
|
||||
UseInterceptors,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiSecurity,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
|
||||
import {
|
||||
QRCodeSessionParam,
|
||||
@@ -19,6 +24,9 @@ import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
|
||||
import {
|
||||
PasskeyAssertionRequest,
|
||||
PasskeyChallenge,
|
||||
PasskeyConfirmationResponse,
|
||||
QRCodeFormat,
|
||||
QRCodeQuery,
|
||||
QRCodeValue,
|
||||
@@ -68,6 +76,54 @@ class AuthController {
|
||||
) {
|
||||
return session.requestCode(request.phoneNumber, request.method, request);
|
||||
}
|
||||
|
||||
@Get('passkey/challenge')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Get the pending passkey (WebAuthn) challenge.',
|
||||
description:
|
||||
'Available while the session is in PASSKEY_REQUIRED status. ' +
|
||||
'Pass the challenge to navigator.credentials.get({ publicKey: challenge }) ' +
|
||||
'on the https://web.whatsapp.com origin.',
|
||||
})
|
||||
@ApiOkResponse({ type: PasskeyChallenge })
|
||||
getPasskeyChallenge(@SessionParam session: WhatsappSession) {
|
||||
return session.getPasskeyChallenge();
|
||||
}
|
||||
|
||||
@Post('passkey')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Submit a WebAuthn passkey assertion to finish pairing.',
|
||||
})
|
||||
submitPasskey(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Body() request: PasskeyAssertionRequest,
|
||||
) {
|
||||
return session.sendPasskeyResponse(JSON.stringify(request));
|
||||
}
|
||||
|
||||
@Get('passkey/confirmation')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Get the pending passkey confirmation code.',
|
||||
description:
|
||||
'Available while the session is in PASSKEY_CONFIRMATION_REQUIRED status. ' +
|
||||
'Most pairings skip this step - WhatsApp confirms them right after the assertion.',
|
||||
})
|
||||
@ApiOkResponse({ type: PasskeyConfirmationResponse })
|
||||
getPasskeyConfirmation(@SessionParam session: WhatsappSession) {
|
||||
return session.getPasskeyConfirmation();
|
||||
}
|
||||
|
||||
@Post('passkey/confirm')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Confirm passkey pairing (only needed for the manual code case).',
|
||||
})
|
||||
confirmPasskey(@SessionParam session: WhatsappSession) {
|
||||
return session.confirmPasskey();
|
||||
}
|
||||
}
|
||||
|
||||
export { AuthController };
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
JoinGroupRequest,
|
||||
JoinGroupResponse,
|
||||
ParticipantsRequest,
|
||||
SettingsMemberAddMode,
|
||||
SettingsSecurityChangeInfo,
|
||||
SubjectRequest,
|
||||
} from '../structures/groups.dto';
|
||||
@@ -309,6 +310,39 @@ export class GroupsController {
|
||||
return session.getMessagesAdminsOnly(id);
|
||||
}
|
||||
|
||||
@Put(':id/settings/security/member-add-mode')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@CheckPolicies(CanSession(Action.Send, FromParam('session')))
|
||||
@ApiOperation({
|
||||
summary: 'Update settings - who can add new members',
|
||||
description:
|
||||
'Updates the group settings for who can add new members to the group - all members or admins only.',
|
||||
})
|
||||
setMemberAddMode(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Body() request: SettingsMemberAddMode,
|
||||
) {
|
||||
return session.setMemberAddMode(id, request.membersCanAddNewMember);
|
||||
}
|
||||
|
||||
@Get(':id/settings/security/member-add-mode')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@CheckPolicies(CanSession(Action.Read, FromParam('session')))
|
||||
@ApiOperation({
|
||||
summary: 'Get settings - who can add new members',
|
||||
description:
|
||||
'The group settings for who can add new members to the group - all members or admins only.',
|
||||
})
|
||||
getMemberAddMode(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<SettingsMemberAddMode> {
|
||||
return session.getMemberAddMode(id);
|
||||
}
|
||||
|
||||
@Get(':id/invite-code')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
|
||||
@@ -221,10 +221,23 @@ export class MessageHandler {
|
||||
mentions: mentions,
|
||||
};
|
||||
const session = this.session;
|
||||
await session.readMessages(chatId);
|
||||
await session.startTyping({ chatId: chatId, session: '' });
|
||||
// Best effort - do not block sending on read-receipt failure
|
||||
await session.readMessages(chatId).catch((err) => {
|
||||
this.logger.warn(
|
||||
`ChatWoot => WhatsApp: error reading messages in chat '${chatId}': ${err}`,
|
||||
);
|
||||
});
|
||||
await session.startTyping({ chatId: chatId, session: '' }).catch((err) => {
|
||||
this.logger.warn(
|
||||
`ChatWoot => WhatsApp: error starting typing in chat '${chatId}': ${err}`,
|
||||
);
|
||||
});
|
||||
await sleep(2000);
|
||||
await session.stopTyping({ chatId: chatId, session: '' });
|
||||
await session.stopTyping({ chatId: chatId, session: '' }).catch((err) => {
|
||||
this.logger.warn(
|
||||
`ChatWoot => WhatsApp: error stopping typing in chat '${chatId}': ${err}`,
|
||||
);
|
||||
});
|
||||
const msg = await session.sendText(request);
|
||||
return msg;
|
||||
}
|
||||
|
||||
@@ -31,4 +31,28 @@ export class McpController {
|
||||
});
|
||||
return ImageMcpResponse(Buffer.from(response.data));
|
||||
}
|
||||
|
||||
protected async scopedApiKey(
|
||||
url: string,
|
||||
session: string,
|
||||
): Promise<string | null> {
|
||||
const response = await this.request({
|
||||
method: 'POST',
|
||||
url: url,
|
||||
data: { session: session },
|
||||
});
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response.data?.key ?? null;
|
||||
}
|
||||
// e.g. 403 (caller lacks the scope) or 422 (session missing) → fall back
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async mediaApiKey(session: string): Promise<string | null> {
|
||||
return this.scopedApiKey('/api/keys/media', session);
|
||||
}
|
||||
|
||||
protected async controlApiKey(session: string): Promise<string | null> {
|
||||
return this.scopedApiKey('/api/keys/control', session);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { PresenceTools } from '@waha/apps/mcp/tools/presence.tools';
|
||||
import { ProfileTools } from '@waha/apps/mcp/tools/profile.tools';
|
||||
import { StatusTools } from '@waha/apps/mcp/tools/status.tools';
|
||||
import { ServerTools } from '@waha/apps/mcp/tools/server.tools';
|
||||
import { KeysTools } from '@waha/apps/mcp/tools/keys.tools';
|
||||
|
||||
@Injectable()
|
||||
export class McpService {
|
||||
@@ -42,6 +43,7 @@ export class McpService {
|
||||
new ProfileTools(api),
|
||||
new StatusTools(api),
|
||||
new ServerTools(api),
|
||||
new KeysTools(api),
|
||||
]);
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined, // stateless
|
||||
|
||||
@@ -3,17 +3,24 @@ import { WAHASelf } from '@waha/apps/app_sdk/waha/WAHASelf';
|
||||
import { McpController } from '@waha/apps/mcp/decorators/controller';
|
||||
import { Tool } from '@waha/apps/mcp/decorators/tool';
|
||||
import {
|
||||
AuthPasskeyChallengeInput,
|
||||
AuthPasskeyConfirmationInput,
|
||||
AuthPasskeyConfirmInput,
|
||||
AuthPasskeySubmitInput,
|
||||
AuthQRInput,
|
||||
AuthRequestCodeInput,
|
||||
ScreenshotInput,
|
||||
} from '@waha/apps/mcp/tools/auth.zod';
|
||||
|
||||
function AuthContent(key: string): any {
|
||||
function AuthContent(key: string | null): any {
|
||||
const open = key
|
||||
? `add "?x-api-key=${key}" to the query params (this is a control-only key scoped to this session)`
|
||||
: `append "?x-api-key=YOUR_API_KEY" to the query params, using the key you already have`;
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text: `
|
||||
You can either ask the user to scan a QR code or provide a phone number and call auth-request-code. auth-request-code is preferable, so ask for the phone number and pass it in international format without +.
|
||||
If the user wants to open the QR code or screenshot in a browser, add "?x-api-key=${key}" to the query params.
|
||||
If the user wants to open the QR code or screenshot in a browser, ${open}.
|
||||
`,
|
||||
};
|
||||
}
|
||||
@@ -39,7 +46,8 @@ export class AuthTools extends McpController {
|
||||
})
|
||||
async authQR({ session }: z.infer<typeof AuthQRInput>) {
|
||||
const result = await this.imageRequest(`/api/${session}/auth/qr`);
|
||||
result.content.push(AuthContent(this.api.key));
|
||||
const key = await this.controlApiKey(session);
|
||||
result.content.push(AuthContent(key));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -58,7 +66,8 @@ export class AuthTools extends McpController {
|
||||
const result = await this.imageRequest(
|
||||
`/api/screenshot?session=${session}`,
|
||||
);
|
||||
result.content.push(AuthContent(this.api.key));
|
||||
const key = await this.controlApiKey(session);
|
||||
result.content.push(AuthContent(key));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -94,4 +103,95 @@ export class AuthTools extends McpController {
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Tool('auth-passkey-challenge', {
|
||||
title: 'Get passkey challenge',
|
||||
description:
|
||||
'Get the pending passkey (WebAuthn) challenge for a session in PASSKEY_REQUIRED status. ' +
|
||||
'Fails with 422 when nothing is pending. ' +
|
||||
'You cannot sign the challenge yourself - the assertion has to be produced by an authenticator ' +
|
||||
'on the https://web.whatsapp.com origin (the WAHA browser extension, or the DevTools fallback). ' +
|
||||
'Hand the challenge to the user, then submit the result with auth-passkey-submit.',
|
||||
inputSchema: AuthPasskeyChallengeInput,
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
},
|
||||
})
|
||||
async passkeyChallenge({
|
||||
session,
|
||||
}: z.infer<typeof AuthPasskeyChallengeInput>) {
|
||||
return this.textRequest({
|
||||
method: 'GET',
|
||||
url: `/api/${session}/auth/passkey/challenge`,
|
||||
});
|
||||
}
|
||||
|
||||
@Tool('auth-passkey-submit', {
|
||||
title: 'Submit passkey assertion',
|
||||
description:
|
||||
'Submit the WebAuthn assertion produced by navigator.credentials.get() to finish passkey pairing. ' +
|
||||
'Get the challenge from auth-passkey-challenge first. ' +
|
||||
'After this the session usually goes straight to WORKING; ' +
|
||||
'if it goes to PASSKEY_CONFIRMATION_REQUIRED instead, follow up with auth-passkey-confirmation.',
|
||||
inputSchema: AuthPasskeySubmitInput,
|
||||
annotations: {
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
idempotentHint: false,
|
||||
},
|
||||
})
|
||||
async passkeySubmit({
|
||||
session,
|
||||
...body
|
||||
}: z.infer<typeof AuthPasskeySubmitInput>) {
|
||||
return this.textRequest({
|
||||
method: 'POST',
|
||||
url: `/api/${session}/auth/passkey`,
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
|
||||
@Tool('auth-passkey-confirmation', {
|
||||
title: 'Get passkey confirmation code',
|
||||
description:
|
||||
'Get the pending passkey confirmation code for a session in PASSKEY_CONFIRMATION_REQUIRED status. ' +
|
||||
'Fails with 422 when nothing is pending. ' +
|
||||
'Show the code to the user, ask them to check it matches the one on their phone, ' +
|
||||
'then call auth-passkey-confirm.',
|
||||
inputSchema: AuthPasskeyConfirmationInput,
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
},
|
||||
})
|
||||
async passkeyConfirmation({
|
||||
session,
|
||||
}: z.infer<typeof AuthPasskeyConfirmationInput>) {
|
||||
return this.textRequest({
|
||||
method: 'GET',
|
||||
url: `/api/${session}/auth/passkey/confirmation`,
|
||||
});
|
||||
}
|
||||
|
||||
@Tool('auth-passkey-confirm', {
|
||||
title: 'Confirm passkey pairing',
|
||||
description:
|
||||
'Finish passkey pairing after the user confirmed the code matches the one shown on their phone. ' +
|
||||
'Only call it once the user has verified the code from auth-passkey-confirmation.',
|
||||
inputSchema: AuthPasskeyConfirmInput,
|
||||
annotations: {
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
},
|
||||
})
|
||||
async passkeyConfirm({ session }: z.infer<typeof AuthPasskeyConfirmInput>) {
|
||||
return this.textRequest({
|
||||
method: 'POST',
|
||||
url: `/api/${session}/auth/passkey/confirm`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { z } from 'zod';
|
||||
import { DtoToZod } from '@waha/apps/mcp/schemas/DtoToZod';
|
||||
import { RequestCodeRequest } from '@waha/structures/auth.dto';
|
||||
import {
|
||||
PasskeyAssertionRequest,
|
||||
RequestCodeRequest,
|
||||
} from '@waha/structures/auth.dto';
|
||||
|
||||
export const AuthQRInput = z.object({
|
||||
session: z.string(),
|
||||
@@ -13,3 +16,19 @@ export const ScreenshotInput = z.object({
|
||||
export const AuthRequestCodeInput = DtoToZod(RequestCodeRequest).extend({
|
||||
session: z.string(),
|
||||
});
|
||||
|
||||
export const AuthPasskeyChallengeInput = z.object({
|
||||
session: z.string(),
|
||||
});
|
||||
|
||||
export const AuthPasskeyConfirmationInput = z.object({
|
||||
session: z.string(),
|
||||
});
|
||||
|
||||
export const AuthPasskeySubmitInput = DtoToZod(PasskeyAssertionRequest).extend({
|
||||
session: z.string(),
|
||||
});
|
||||
|
||||
export const AuthPasskeyConfirmInput = z.object({
|
||||
session: z.string(),
|
||||
});
|
||||
@@ -18,10 +18,22 @@ import {
|
||||
UnpinMessageInput,
|
||||
} from '@waha/apps/mcp/tools/chats.zod';
|
||||
|
||||
function FetchMediaUsingApiKeyContent(key: string) {
|
||||
function FetchMediaContent(key: string | null) {
|
||||
if (key) {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text:
|
||||
`To fetch media use "X-Api-Key: ${key}" HTTP header. ` +
|
||||
`To open it in a browser add "?x-api-key=${key}" to the query params. ` +
|
||||
`This is a media-only API key: it can ONLY download files for this session — ` +
|
||||
`it cannot read messages, send, or control the session.`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text: `To fetch media use "X-Api-Key: ${key}" HTTP header. If the user wants to open it - add "?x-api-key=${key}" to query params`,
|
||||
text:
|
||||
`To fetch media, use your existing WAHA API key in the "X-Api-Key" HTTP header ` +
|
||||
`(or append "?x-api-key=YOUR_API_KEY" to open it in a browser).`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -170,7 +182,8 @@ export class ChatTools extends McpController {
|
||||
JSON.stringify({ status: response.status, response: responseText }),
|
||||
);
|
||||
if (query.downloadMedia) {
|
||||
result.content.push(FetchMediaUsingApiKeyContent(this.api.key));
|
||||
const mediaKey = await this.mediaApiKey(session);
|
||||
result.content.push(FetchMediaContent(mediaKey));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -219,7 +232,8 @@ export class ChatTools extends McpController {
|
||||
params: query,
|
||||
});
|
||||
if (query.downloadMedia) {
|
||||
result.content.push(FetchMediaUsingApiKeyContent(this.api.key));
|
||||
const mediaKey = await this.mediaApiKey(session);
|
||||
result.content.push(FetchMediaContent(mediaKey));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { z } from 'zod';
|
||||
import { WAHASelf } from '@waha/apps/app_sdk/waha/WAHASelf';
|
||||
import { McpController } from '@waha/apps/mcp/decorators/controller';
|
||||
import { Tool } from '@waha/apps/mcp/decorators/tool';
|
||||
import { TextMcpResponse } from '@waha/apps/mcp/responses';
|
||||
import { ScopedKeyInput } from '@waha/apps/mcp/tools/keys.zod';
|
||||
|
||||
export class KeysTools extends McpController {
|
||||
constructor(api: WAHASelf) {
|
||||
super(api);
|
||||
}
|
||||
|
||||
@Tool('keys-get-scoped-key', {
|
||||
title: 'Get a scoped API key',
|
||||
description:
|
||||
'Create or get a minimal, scoped API key for a session. ' +
|
||||
'Use "media" scope for a download-only key to fetch media files, ' +
|
||||
'or "control" scope for a control-only key to open the QR code / screenshot in a browser. ' +
|
||||
'The returned key is far weaker than your own key and is safe to hand to the user for that single purpose.',
|
||||
inputSchema: ScopedKeyInput,
|
||||
annotations: {
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
},
|
||||
})
|
||||
async getScopedKey({ session, scope }: z.infer<typeof ScopedKeyInput>) {
|
||||
let key: string | null = null;
|
||||
if (scope === 'media') {
|
||||
key = await this.mediaApiKey(session);
|
||||
} else {
|
||||
key = await this.controlApiKey(session);
|
||||
}
|
||||
if (!key) {
|
||||
return TextMcpResponse(
|
||||
JSON.stringify({
|
||||
error:
|
||||
'Could not mint a scoped API key. Check that the session exists and that your key has access to it.',
|
||||
}),
|
||||
);
|
||||
}
|
||||
return TextMcpResponse(JSON.stringify({ scope: scope, key: key }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const SessionField = z.string().describe('Session name');
|
||||
|
||||
export const ScopedKeyInput = z.object({
|
||||
session: SessionField,
|
||||
scope: z
|
||||
.enum(['media', 'control'])
|
||||
.describe(
|
||||
'Scope of the key. ' +
|
||||
'"media" — download-only key for fetching media files of the session. ' +
|
||||
'"control" — control-only key to open QR code / screenshot in a browser.',
|
||||
),
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ReachoutTimelockData } from '@waha/structures/sessions.dto';
|
||||
import { LongTimeout, setLongTimeout } from '@waha/utils/promiseTimeout';
|
||||
import { EnsureMilliseconds } from '@waha/utils/timehelper';
|
||||
import * as lodash from 'lodash';
|
||||
import { Logger } from 'pino';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
/**
|
||||
* WhatsApp "reachout timelock"
|
||||
* The account is restricted from messaging new contacts for a period of time (the cause of 463 errors on send).
|
||||
* Keeps the latest known state, expires it when the enforcement ends and emits changes.
|
||||
*/
|
||||
export class ReachoutTimelockTracker {
|
||||
private timelock: ReachoutTimelockData | null = null;
|
||||
private timeout?: LongTimeout;
|
||||
private changes: Subject<ReachoutTimelockData | null> = new Subject();
|
||||
|
||||
constructor(private logger: Logger) {}
|
||||
|
||||
get value(): ReachoutTimelockData | null {
|
||||
return this.timelock;
|
||||
}
|
||||
|
||||
get changes$(): Observable<ReachoutTimelockData | null> {
|
||||
return this.changes;
|
||||
}
|
||||
|
||||
update(timelock: ReachoutTimelockData | null) {
|
||||
if (timelock && !timelock.isActive && !this.timelock) {
|
||||
// No enforcement has been seen for the account - keep it null instead of storing an inactive record
|
||||
// (startup fetch on a clean account lands here)
|
||||
return;
|
||||
}
|
||||
if (lodash.isEqual(this.timelock, timelock)) {
|
||||
return;
|
||||
}
|
||||
this.timeout?.clear();
|
||||
if (timelock?.isActive && timelock.timeEnforcementEnds) {
|
||||
const endsAtMs = EnsureMilliseconds(timelock.timeEnforcementEnds);
|
||||
const delay = Math.max(endsAtMs - Date.now(), 0);
|
||||
this.timeout = setLongTimeout(() => this.expire(), delay).unref();
|
||||
}
|
||||
this.apply(timelock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the expiry timer, keeping the last known state.
|
||||
*/
|
||||
stop() {
|
||||
this.timeout?.clear();
|
||||
}
|
||||
|
||||
private expire() {
|
||||
if (!this.timelock?.isActive) {
|
||||
return;
|
||||
}
|
||||
this.apply({ ...this.timelock, isActive: false });
|
||||
}
|
||||
|
||||
private apply(timelock: ReachoutTimelockData | null) {
|
||||
this.timelock = timelock;
|
||||
this.logger.info(
|
||||
{ reachoutTimelock: timelock },
|
||||
'Reachout timelock updated',
|
||||
);
|
||||
this.changes.next(timelock);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import { WAHAEvents, WAHASessionStatus } from '@waha/structures/enums.dto';
|
||||
import {
|
||||
MeInfo,
|
||||
ReachoutTimelockData,
|
||||
ReachoutTimelockEnforcementType,
|
||||
} from '@waha/structures/sessions.dto';
|
||||
import { WASessionStatusBody } from '@waha/structures/webhooks.dto';
|
||||
|
||||
const logger: any = {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
};
|
||||
logger.child = () => logger;
|
||||
|
||||
const BaseSession = WhatsappSession as unknown as new (params: any) => any;
|
||||
|
||||
class TestSession extends BaseSession {
|
||||
public getSessionMeInfo(): MeInfo | null {
|
||||
// pushName and id must be set so the SESSION_STATUS pipeline does not delay WORKING statuses
|
||||
return { id: '123@c.us', pushName: 'Test' };
|
||||
}
|
||||
|
||||
public setStatusPublic(status: WAHASessionStatus) {
|
||||
this.setStatus(status);
|
||||
}
|
||||
|
||||
public updateReachoutTimelockPublic(timelock: ReachoutTimelockData | null) {
|
||||
this.reachoutTimelock.update(timelock);
|
||||
}
|
||||
}
|
||||
|
||||
function buildSession(): TestSession {
|
||||
return new TestSession({
|
||||
name: 'test',
|
||||
printQR: false,
|
||||
loggerBuilder: { child: () => logger },
|
||||
sessionStore: null,
|
||||
mediaManager: null,
|
||||
sessionConfig: null,
|
||||
engineConfig: null,
|
||||
ignore: {},
|
||||
});
|
||||
}
|
||||
|
||||
const ACTIVE_TIMELOCK: ReachoutTimelockData = {
|
||||
enforcementType: ReachoutTimelockEnforcementType.RESTRICT_ALL_COMPANIONS,
|
||||
isActive: true,
|
||||
timeEnforcementEnds: Math.floor(Date.now() / 1000) + 3600,
|
||||
};
|
||||
|
||||
describe('WhatsappSession reachout timelock', () => {
|
||||
let session: TestSession;
|
||||
let statuses: WASessionStatusBody[];
|
||||
|
||||
beforeEach(() => {
|
||||
session = buildSession();
|
||||
statuses = [];
|
||||
session
|
||||
.getEventObservable(WAHAEvents.SESSION_STATUS)
|
||||
.subscribe((body: WASessionStatusBody) => statuses.push(body));
|
||||
});
|
||||
|
||||
it('re-issues WORKING with data when a timelock arrives', () => {
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.updateReachoutTimelockPublic(ACTIVE_TIMELOCK);
|
||||
|
||||
expect(statuses).toHaveLength(2);
|
||||
expect(statuses[1].status).toEqual(WAHASessionStatus.WORKING);
|
||||
expect(statuses[1].data).toEqual({ reachoutTimelock: ACTIVE_TIMELOCK });
|
||||
});
|
||||
|
||||
it('keeps attaching the timelock on plain WORKING assignments', () => {
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.updateReachoutTimelockPublic(ACTIVE_TIMELOCK);
|
||||
// Reconnect flows assign 'status = WORKING' with no data
|
||||
session.setStatusPublic(WAHASessionStatus.STARTING);
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
|
||||
const last = statuses.at(-1);
|
||||
expect(last.status).toEqual(WAHASessionStatus.WORKING);
|
||||
expect(last.data).toEqual({ reachoutTimelock: ACTIVE_TIMELOCK });
|
||||
});
|
||||
|
||||
it('collapses consecutive WORKING statuses with the same data', () => {
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.updateReachoutTimelockPublic(ACTIVE_TIMELOCK);
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
|
||||
expect(statuses).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('ignores duplicate timelock updates', () => {
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.updateReachoutTimelockPublic(ACTIVE_TIMELOCK);
|
||||
session.updateReachoutTimelockPublic({ ...ACTIVE_TIMELOCK });
|
||||
|
||||
expect(statuses).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('ignores an inactive timelock when none has been seen', () => {
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.updateReachoutTimelockPublic({
|
||||
enforcementType: ReachoutTimelockEnforcementType.DEFAULT,
|
||||
isActive: false,
|
||||
timeEnforcementEnds: null,
|
||||
});
|
||||
|
||||
expect(statuses).toHaveLength(1);
|
||||
expect(statuses[0].data).toBeNull();
|
||||
});
|
||||
|
||||
it('re-issues WORKING when the timelock is lifted by an event', () => {
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.updateReachoutTimelockPublic(ACTIVE_TIMELOCK);
|
||||
const lifted = { ...ACTIVE_TIMELOCK, isActive: false };
|
||||
session.updateReachoutTimelockPublic(lifted);
|
||||
|
||||
expect(statuses).toHaveLength(3);
|
||||
expect(statuses[2].data).toEqual({ reachoutTimelock: lifted });
|
||||
});
|
||||
|
||||
it('marks the timelock inactive when the enforcement expires', () => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
const endsAt = Math.floor(Date.now() / 1000) + 600;
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.updateReachoutTimelockPublic({
|
||||
...ACTIVE_TIMELOCK,
|
||||
timeEnforcementEnds: endsAt,
|
||||
});
|
||||
|
||||
jest.advanceTimersByTime(601 * 1000);
|
||||
|
||||
const last = statuses.at(-1);
|
||||
expect(last.data.reachoutTimelock.isActive).toBe(false);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not fire the expiry timer after the session stops', () => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
session.setStatusPublic(WAHASessionStatus.WORKING);
|
||||
session.updateReachoutTimelockPublic({
|
||||
...ACTIVE_TIMELOCK,
|
||||
timeEnforcementEnds: Math.floor(Date.now() / 1000) + 600,
|
||||
});
|
||||
session.setStatusPublic(WAHASessionStatus.STOPPED);
|
||||
const count = statuses.length;
|
||||
|
||||
jest.advanceTimersByTime(601 * 1000);
|
||||
|
||||
expect(statuses).toHaveLength(count);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
+117
-25
@@ -1,3 +1,4 @@
|
||||
import { ReachoutTimelockTracker } from '@waha/core/abc/ReachoutTimelockTracker';
|
||||
import { getBrowserExecutablePath as getBrowserExecutablePathAutodetect } from '@waha/core/abc/session.browser';
|
||||
import { IMediaConverter } from '@waha/core/media/IConverter';
|
||||
import { Ffmpeg } from '@waha/core/utils/ffmpeg';
|
||||
@@ -63,6 +64,10 @@ import {
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { MessageId } from 'whatsapp-web.js';
|
||||
|
||||
import {
|
||||
PasskeyChallenge,
|
||||
PasskeyConfirmationResponse,
|
||||
} from '../../structures/auth.dto';
|
||||
import {
|
||||
ChatRequest,
|
||||
CheckNumberStatusQuery,
|
||||
@@ -103,6 +108,7 @@ import {
|
||||
GroupParticipant,
|
||||
GroupsListFields,
|
||||
ParticipantsRequest,
|
||||
SettingsMemberAddMode,
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '../../structures/groups.dto';
|
||||
import { WAHAChatPresences } from '../../structures/presence.dto';
|
||||
@@ -163,6 +169,19 @@ export interface SessionParams {
|
||||
ignore: IgnoreJidConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* A status change, along with the extra info that belongs to that status
|
||||
* (if any) - like the passkey challenge for PASSKEY_REQUIRED.
|
||||
*/
|
||||
interface SessionStatusUpdate {
|
||||
status: WAHASessionStatus;
|
||||
data: any;
|
||||
}
|
||||
|
||||
interface SessionStatusUpdatePoint extends SessionStatusUpdate {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export abstract class WhatsappSession {
|
||||
public engine: WAHAEngine;
|
||||
|
||||
@@ -178,6 +197,8 @@ export abstract class WhatsappSession {
|
||||
protected jids: JidFilter;
|
||||
|
||||
private _status: WAHASessionStatus;
|
||||
private _statusData: any = null;
|
||||
protected reachoutTimelock: ReachoutTimelockTracker;
|
||||
private _presence:
|
||||
| WAHAPresenceStatus.ONLINE
|
||||
| WAHAPresenceStatus.OFFLINE
|
||||
@@ -190,7 +211,7 @@ export abstract class WhatsappSession {
|
||||
|
||||
private shouldPrintQR: boolean;
|
||||
protected events2: DefaultMap<WAHAEvents, SwitchObservable<any>>;
|
||||
private status$: Subject<WAHASessionStatus>;
|
||||
private status$: Subject<SessionStatusUpdate>;
|
||||
protected profilePictures: NodeCache = new NodeCache({
|
||||
stdTTL: 24 * 60 * 60, // 1 day
|
||||
});
|
||||
@@ -217,13 +238,22 @@ export abstract class WhatsappSession {
|
||||
ignore,
|
||||
}: SessionParams) {
|
||||
this._status = WAHASessionStatus.STOPPED;
|
||||
this.status$ = new Subject<WAHASessionStatus>();
|
||||
this.status$ = new Subject<SessionStatusUpdate>();
|
||||
|
||||
this.name = name;
|
||||
this.proxyConfig = proxyConfig;
|
||||
this.loggerBuilder = loggerBuilder;
|
||||
this.logger = loggerBuilder.child({ name: 'WhatsappSession' });
|
||||
this.mediaConverter = new Ffmpeg(this.name, this.logger);
|
||||
this.reachoutTimelock = new ReachoutTimelockTracker(this.logger);
|
||||
this.reachoutTimelock.changes$.subscribe((timelock) => {
|
||||
if (this.status === WAHASessionStatus.WORKING) {
|
||||
// Re-issue WORKING so 'session.status' consumers get the update
|
||||
this.setStatus(WAHASessionStatus.WORKING, {
|
||||
reachoutTimelock: timelock,
|
||||
});
|
||||
}
|
||||
});
|
||||
this.events2 = new DefaultMap<WAHAEvents, SwitchObservable<any>>(
|
||||
(key) =>
|
||||
new SwitchObservable((obs$) => {
|
||||
@@ -252,46 +282,56 @@ export abstract class WhatsappSession {
|
||||
// Wait for WORKING status to get all the info
|
||||
// https://github.com/devlikeapro/waha/issues/409
|
||||
.pipe(
|
||||
switchMap((status: WAHASessionStatus) => {
|
||||
switchMap((update: SessionStatusUpdate) => {
|
||||
const me = this.getSessionMeInfo();
|
||||
const hasMe = !!me?.pushName && !!me?.id;
|
||||
// Delay WORKING by 1 second if condition is met
|
||||
// Usually we get WORKING with all the info after
|
||||
if (status === WAHASessionStatus.WORKING && !hasMe) {
|
||||
return of(status).pipe(delay(2000));
|
||||
if (update.status === WAHASessionStatus.WORKING && !hasMe) {
|
||||
return of(update).pipe(delay(2000));
|
||||
}
|
||||
return of(status);
|
||||
return of(update);
|
||||
}),
|
||||
// Remove consecutive duplicate WORKING statuses
|
||||
// Remove consecutive duplicate WORKING statuses, but let through WORKING re-issued with new data
|
||||
distinctUntilChanged(
|
||||
(prev, curr) => prev === curr && curr === WAHASessionStatus.WORKING,
|
||||
(prev, curr) =>
|
||||
prev.status === curr.status &&
|
||||
curr.status === WAHASessionStatus.WORKING &&
|
||||
lodash.isEqual(prev.data, curr.data),
|
||||
),
|
||||
// attach current time (ms)
|
||||
timestamp(),
|
||||
map(
|
||||
({ value, timestamp }) =>
|
||||
({
|
||||
status: value,
|
||||
status: value.status,
|
||||
timestamp: timestamp,
|
||||
}) as SessionStatusPoint,
|
||||
data: value.data,
|
||||
}) as SessionStatusUpdatePoint,
|
||||
),
|
||||
// keep the last 3 entries
|
||||
scan<SessionStatusPoint, SessionStatusPoint[]>(
|
||||
(statuses, status: SessionStatusPoint) => {
|
||||
const next = [...statuses, status];
|
||||
scan<SessionStatusUpdatePoint, SessionStatusUpdatePoint[]>(
|
||||
(points, point: SessionStatusUpdatePoint) => {
|
||||
const next = [...points, point];
|
||||
return next.length > 3 ? next.slice(-3) : next;
|
||||
},
|
||||
[],
|
||||
),
|
||||
// shape final payload
|
||||
map(
|
||||
(statuses) =>
|
||||
({
|
||||
name: this.name,
|
||||
status: statuses.at(-1)?.status, // current
|
||||
statuses: statuses,
|
||||
}) as WASessionStatusBody,
|
||||
),
|
||||
map((points) => {
|
||||
const current = points.at(-1); // current
|
||||
return {
|
||||
name: this.name,
|
||||
status: current?.status,
|
||||
statuses: points.map(
|
||||
(point): SessionStatusPoint => ({
|
||||
status: point.status,
|
||||
timestamp: point.timestamp,
|
||||
}),
|
||||
),
|
||||
data: current?.data ?? null,
|
||||
} as WASessionStatusBody;
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -311,20 +351,48 @@ export abstract class WhatsappSession {
|
||||
return this.events2.get(event);
|
||||
}
|
||||
|
||||
public set status(value: WAHASessionStatus) {
|
||||
if (this.unpairing && value !== WAHASessionStatus.STOPPED) {
|
||||
/**
|
||||
* Set the status along with the extra info that belongs to it.
|
||||
* Plain 'status = value' assignments go through here without data,
|
||||
* so the data is dropped as soon as the session moves on.
|
||||
*/
|
||||
protected setStatus(status: WAHASessionStatus, data: any = null) {
|
||||
if (this.unpairing && status !== WAHASessionStatus.STOPPED) {
|
||||
// In case of unpairing
|
||||
// wait for STOPPED event, ignore the rest
|
||||
return;
|
||||
}
|
||||
this._status = value;
|
||||
this.status$.next(value);
|
||||
if (
|
||||
status === WAHASessionStatus.WORKING &&
|
||||
data == null &&
|
||||
this.reachoutTimelock.value?.isActive
|
||||
) {
|
||||
// Plain 'status = WORKING' assignments (reconnects) must keep carrying the active timelock info
|
||||
data = { reachoutTimelock: this.reachoutTimelock.value };
|
||||
}
|
||||
if (
|
||||
status === WAHASessionStatus.STOPPED ||
|
||||
status === WAHASessionStatus.FAILED
|
||||
) {
|
||||
this.reachoutTimelock?.stop();
|
||||
}
|
||||
this._status = status;
|
||||
this._statusData = data;
|
||||
this.status$.next({ status: status, data: data });
|
||||
}
|
||||
|
||||
public set status(value: WAHASessionStatus) {
|
||||
this.setStatus(value);
|
||||
}
|
||||
|
||||
public get status() {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
public get statusData() {
|
||||
return this._statusData;
|
||||
}
|
||||
|
||||
protected set presence(value: WAHAPresenceStatus) {
|
||||
switch (value) {
|
||||
case null:
|
||||
@@ -450,6 +518,22 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getPasskeyChallenge(): PasskeyChallenge {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public async sendPasskeyResponse(responseJson: string): Promise<void> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public async confirmPasskey(): Promise<void> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getPasskeyConfirmation(): PasskeyConfirmationResponse {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
abstract getScreenshot(): Promise<Buffer>;
|
||||
|
||||
public getSessionMeInfo(): MeInfo | null {
|
||||
@@ -948,6 +1032,14 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getMemberAddMode(id): Promise<SettingsMemberAddMode> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public setMemberAddMode(id, value) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public deleteGroup(id) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ApiKeyAuthMiddleware } from '@waha/core/auth/api-key-auth.middleware';
|
||||
import { BasicAuthFunction } from '@waha/core/auth/basicAuth';
|
||||
import { WebSocketAuth } from '@waha/core/auth/WebSocketAuth';
|
||||
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
|
||||
import { NowebEngineConfigService } from '@waha/core/config/NowebEngineConfigService';
|
||||
import { WPPEngineConfigService } from '@waha/core/config/WPPEngineConfigService';
|
||||
import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService';
|
||||
import { MediaLocalStorageModule } from '@waha/core/media/local/media.local.storage.module';
|
||||
@@ -205,6 +206,7 @@ export const PROVIDERS_BASE: Provider[] = [
|
||||
WebJSEngineConfigService,
|
||||
WPPEngineConfigService,
|
||||
GowsEngineConfigService,
|
||||
NowebEngineConfigService,
|
||||
WhatsappConfigService,
|
||||
EngineConfigService,
|
||||
WebsocketGatewayCore,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { WAVersion } from '@adiwajshing/baileys';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { NowebConfig } from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import {
|
||||
isWaVersionHigher,
|
||||
parseWaVersion,
|
||||
} from '@waha/core/engines/noweb/waversion';
|
||||
import { parseBool } from '@waha/helpers';
|
||||
import esm from '@waha/vendor/esm';
|
||||
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
|
||||
|
||||
@Injectable()
|
||||
export class NowebEngineConfigService {
|
||||
private config: NowebConfig | null = null;
|
||||
|
||||
constructor(
|
||||
protected configService: ConfigService,
|
||||
@InjectPinoLogger('NowebEngineConfigService')
|
||||
protected logger: PinoLogger,
|
||||
) {}
|
||||
|
||||
getConfig(): NowebConfig {
|
||||
if (!this.config) {
|
||||
const version = this.resolveWaVersion();
|
||||
this.config = { waVersion: version };
|
||||
}
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the WhatsApp Web version to use for NOWEB engine.
|
||||
* Returns undefined when the built-in Baileys version should be used.
|
||||
*/
|
||||
private resolveWaVersion(): WAVersion {
|
||||
const value = this.configService.get<string>('WAHA_NOWEB_WA_VERSION', '');
|
||||
const envVersion = parseWaVersion(value);
|
||||
const version = esm.b.DEFAULT_CONNECTION_CONFIG.version as WAVersion;
|
||||
const force = parseBool(
|
||||
this.configService.get('WAHA_NOWEB_WA_VERSION_FORCE', 'false'),
|
||||
);
|
||||
|
||||
if (!envVersion && value) {
|
||||
this.logger.warn(
|
||||
`WAHA_NOWEB_WA_VERSION='${value}' - invalid format, expected 'x.y.z'.`,
|
||||
);
|
||||
return version;
|
||||
}
|
||||
|
||||
if (!envVersion && force) {
|
||||
this.logger.warn(
|
||||
`WAHA_NOWEB_WA_VERSION_FORCE is set, but WAHA_NOWEB_WA_VERSION is not.`,
|
||||
);
|
||||
return version;
|
||||
}
|
||||
|
||||
if (!envVersion) {
|
||||
return version;
|
||||
}
|
||||
|
||||
if (force && envVersion) {
|
||||
this.logger.debug(
|
||||
`Using WAHA_NOWEB_WA_VERSION because of WAHA_NOWEB_WA_VERSION_FORCE.`,
|
||||
);
|
||||
return envVersion;
|
||||
}
|
||||
|
||||
if (isWaVersionHigher(version, envVersion)) {
|
||||
this.logger.debug(
|
||||
`Built-in wa.version is higher than WAHA_NOWEB_WA_VERSION, using built-in version.`,
|
||||
);
|
||||
return version;
|
||||
}
|
||||
return envVersion;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import * as grpc from '@grpc/grpc-js';
|
||||
import {
|
||||
GowsEventStreamObservable,
|
||||
GowsStreamEndedError,
|
||||
} from '@waha/core/engines/gows/GowsEventStreamObservable';
|
||||
import { EventEmitter } from 'events';
|
||||
import { merge, Subject } from 'rxjs';
|
||||
import { retry } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* Mimics grpc.ClientReadableStream.
|
||||
* The important part is failWithStatus - it reproduces what grpc-js does in
|
||||
* client.js makeServerStreamRequest.onReceiveStatus on a non OK status:
|
||||
* push(null) schedules 'end' on the next tick, then 'error' is emitted
|
||||
* synchronously in the current tick.
|
||||
*/
|
||||
class FakeStream extends EventEmitter {
|
||||
public cancelled = false;
|
||||
|
||||
cancel() {
|
||||
this.cancelled = true;
|
||||
}
|
||||
|
||||
failWithStatus(err: any) {
|
||||
process.nextTick(() => this.emit('end'));
|
||||
this.emit('error', err);
|
||||
}
|
||||
|
||||
endCleanly() {
|
||||
process.nextTick(() => this.emit('end'));
|
||||
}
|
||||
}
|
||||
|
||||
class FakeClient {
|
||||
public closed = false;
|
||||
|
||||
close() {
|
||||
this.closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
function buildLogger(): any {
|
||||
function noop() {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
debug: noop,
|
||||
info: noop,
|
||||
warn: noop,
|
||||
error: noop,
|
||||
setBindings: noop,
|
||||
};
|
||||
}
|
||||
|
||||
function drainTicks(): Promise<void> {
|
||||
return new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
|
||||
function observe(stream: FakeStream, client: FakeClient) {
|
||||
const observable = new GowsEventStreamObservable(buildLogger(), () => ({
|
||||
client: client as any,
|
||||
stream: stream as any,
|
||||
}));
|
||||
// Do not wait a real second for the client to close
|
||||
observable.CLIENT_CLOSE_TIMEOUT = 0;
|
||||
|
||||
const next = jest.fn();
|
||||
const error = jest.fn();
|
||||
const complete = jest.fn();
|
||||
const subscription = observable.subscribe({
|
||||
next: next,
|
||||
error: error,
|
||||
complete: complete,
|
||||
});
|
||||
return {
|
||||
next: next,
|
||||
error: error,
|
||||
complete: complete,
|
||||
subscription: subscription,
|
||||
};
|
||||
}
|
||||
|
||||
describe('GowsEventStreamObservable', () => {
|
||||
it('errors (not completes) when the stream fails with a non OK status', async () => {
|
||||
const stream = new FakeStream();
|
||||
const client = new FakeClient();
|
||||
const { error, complete } = observe(stream, client);
|
||||
|
||||
const err = { code: grpc.status.UNAVAILABLE, message: 'unavailable' };
|
||||
stream.failWithStatus(err);
|
||||
await drainTicks();
|
||||
|
||||
expect(error).toHaveBeenCalledTimes(1);
|
||||
expect(error).toHaveBeenCalledWith(err);
|
||||
expect(complete).not.toHaveBeenCalled();
|
||||
expect(stream.cancelled).toBe(true);
|
||||
expect(client.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('errors (not completes) when the stream ends cleanly', async () => {
|
||||
const stream = new FakeStream();
|
||||
const client = new FakeClient();
|
||||
const { error, complete } = observe(stream, client);
|
||||
|
||||
stream.endCleanly();
|
||||
await drainTicks();
|
||||
|
||||
expect(error).toHaveBeenCalledTimes(1);
|
||||
expect(error.mock.calls[0][0]).toBeInstanceOf(GowsStreamEndedError);
|
||||
expect(complete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not error when we cancel the stream ourselves', async () => {
|
||||
const stream = new FakeStream();
|
||||
const client = new FakeClient();
|
||||
const { error, complete, subscription } = observe(stream, client);
|
||||
|
||||
subscription.unsubscribe();
|
||||
stream.emit('error', { code: grpc.status.CANCELLED });
|
||||
stream.emit('end');
|
||||
await drainTicks();
|
||||
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
expect(complete).not.toHaveBeenCalled();
|
||||
expect(stream.cancelled).toBe(true);
|
||||
expect(client.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('reconnects via retry() when merged with a never ending subject', async () => {
|
||||
const streams = [new FakeStream(), new FakeStream()];
|
||||
const clients = [new FakeClient(), new FakeClient()];
|
||||
let attempt = 0;
|
||||
|
||||
const observable = new GowsEventStreamObservable(buildLogger(), () => {
|
||||
const index = attempt;
|
||||
attempt += 1;
|
||||
return { client: clients[index] as any, stream: streams[index] as any };
|
||||
});
|
||||
observable.CLIENT_CLOSE_TIMEOUT = 0;
|
||||
|
||||
const local$ = new Subject<any>();
|
||||
const next = jest.fn();
|
||||
const error = jest.fn();
|
||||
const subscription = merge(observable, local$)
|
||||
.pipe(retry({ delay: 1 }))
|
||||
.subscribe({ next: next, error: error });
|
||||
|
||||
streams[0].failWithStatus({ code: grpc.status.INTERNAL });
|
||||
await drainTicks();
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
|
||||
expect(attempt).toBe(2);
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
|
||||
streams[1].emit('data', {
|
||||
toObject: () => ({ event: 'Message', data: '{"id":"1"}' }),
|
||||
});
|
||||
await drainTicks();
|
||||
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
expect(next.mock.calls[0][0]).toEqual({
|
||||
event: 'Message',
|
||||
data: { id: '1' },
|
||||
});
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,29 @@
|
||||
import * as grpc from '@grpc/grpc-js';
|
||||
import { rand } from '@waha/core/auth/config';
|
||||
import { messages } from '@waha/core/engines/gows/grpc/gows';
|
||||
import { EnginePayload } from '@waha/structures/webhooks.dto';
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
import { Logger } from 'pino';
|
||||
import { Observable } from 'rxjs';
|
||||
import { rand } from '@waha/core/auth/config';
|
||||
|
||||
/**
|
||||
* Raised when the gRPC stream ends without an error.
|
||||
* The engine event stream is expected to live as long as the session,
|
||||
* so a clean end still means we lost the events and have to reconnect.
|
||||
*/
|
||||
export class GowsStreamEndedError extends Error {
|
||||
constructor() {
|
||||
super('gRPC event stream ended');
|
||||
this.name = 'GowsStreamEndedError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observable that listens to a gRPC stream and emits EnginePayload objects.
|
||||
* Pass a factory function that returns a client and a stream.
|
||||
*
|
||||
* The observable always terminates with an error, never with a completion,
|
||||
* so that an upstream retry() reconnects the stream.
|
||||
*/
|
||||
export class GowsEventStreamObservable extends Observable<EnginePayload> {
|
||||
_client: grpc.Client;
|
||||
@@ -26,63 +41,79 @@ export class GowsEventStreamObservable extends Observable<EnginePayload> {
|
||||
logger.setBindings({ id: rand() });
|
||||
const { client, stream } = factory();
|
||||
this._client = client;
|
||||
const closeTimeout = this.CLIENT_CLOSE_TIMEOUT;
|
||||
|
||||
let closed = false;
|
||||
const cleanup = async (reason: string) => {
|
||||
let terminated = false;
|
||||
let tearingDown = false;
|
||||
|
||||
async function cleanup(reason: string) {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
|
||||
logger.debug({ reason }, 'Cancelling gRPC stream...');
|
||||
logger.debug({ reason: reason }, 'Cancelling gRPC stream...');
|
||||
try {
|
||||
stream.cancel();
|
||||
} catch (err) {
|
||||
logger.warn({ err }, 'Failed to cancel gRPC stream');
|
||||
logger.warn({ err: err }, 'Failed to cancel gRPC stream');
|
||||
}
|
||||
|
||||
logger.debug({ reason }, 'Closing gRPC client...');
|
||||
logger.debug({ reason: reason }, 'Closing gRPC client...');
|
||||
try {
|
||||
client.close();
|
||||
} catch (err) {
|
||||
logger.warn({ err }, 'Failed to close gRPC client');
|
||||
logger.warn({ err: err }, 'Failed to close gRPC client');
|
||||
}
|
||||
|
||||
await sleep(this.CLIENT_CLOSE_TIMEOUT);
|
||||
};
|
||||
await sleep(closeTimeout);
|
||||
}
|
||||
|
||||
// Must run synchronously from the stream handlers.
|
||||
// grpc-js calls stream.push(null) - which schedules 'end' on the next tick -
|
||||
// and only then emits 'error' in the same tick. Erroring the subscriber
|
||||
// right away wins that race, otherwise 'end' completes the observable
|
||||
// and the upstream retry() never reconnects.
|
||||
function terminate(err: Error) {
|
||||
if (terminated) {
|
||||
return;
|
||||
}
|
||||
terminated = true;
|
||||
// Erroring the subscriber runs the teardown below, which cleans up.
|
||||
subscriber.error(err);
|
||||
}
|
||||
|
||||
stream.on('data', (raw) => {
|
||||
setImmediate(() => {
|
||||
const obj = raw.toObject();
|
||||
obj.data = JSON.parse(obj.data);
|
||||
subscriber?.next(obj);
|
||||
subscriber.next(obj);
|
||||
});
|
||||
});
|
||||
|
||||
stream.on('end', (...args) => {
|
||||
logger.debug('Stream ended', args);
|
||||
subscriber?.complete();
|
||||
subscriber = null;
|
||||
void cleanup('end');
|
||||
});
|
||||
|
||||
stream.on('error', async (err: any) => {
|
||||
const CLIENT_CANCELLED_CODE = grpc.status.CANCELLED;
|
||||
if (err.code === CLIENT_CANCELLED_CODE) {
|
||||
logger.debug('Stream cancelled by client');
|
||||
await cleanup('cancelled');
|
||||
stream.on('end', () => {
|
||||
if (tearingDown || terminated) {
|
||||
logger.debug('Stream ended');
|
||||
return;
|
||||
}
|
||||
logger.error(err, 'Stream error');
|
||||
await cleanup('error');
|
||||
// Give some time to node event loop to process the error
|
||||
await sleep(100);
|
||||
subscriber?.error(err);
|
||||
subscriber = null;
|
||||
logger.error('Stream ended unexpectedly, reconnecting...');
|
||||
terminate(new GowsStreamEndedError());
|
||||
});
|
||||
|
||||
return async () => {
|
||||
await cleanup('teardown');
|
||||
stream.on('error', (err: any) => {
|
||||
if (tearingDown || terminated) {
|
||||
// We cancelled the stream ourselves, no need to reconnect
|
||||
logger.debug({ err: err }, 'Stream cancelled by client');
|
||||
return;
|
||||
}
|
||||
logger.error(err, 'Stream error, reconnecting...');
|
||||
terminate(err);
|
||||
});
|
||||
|
||||
return () => {
|
||||
tearingDown = true;
|
||||
void cleanup('teardown');
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,5 +18,9 @@ export function BuildEventStreamClient(
|
||||
return new messages.EventStreamClient(address, credentials, {
|
||||
'grpc.max_send_message_length': 128 * 1024 * 1024,
|
||||
'grpc.max_receive_message_length': 128 * 1024 * 1024,
|
||||
// Detect a hung server, so the stream errors out and we reconnect
|
||||
'grpc.keepalive_time_ms': 30_000,
|
||||
'grpc.keepalive_timeout_ms': 10_000,
|
||||
'grpc.keepalive_permit_without_calls': 1,
|
||||
});
|
||||
}
|
||||
@@ -1132,6 +1132,99 @@ export namespace messages {
|
||||
return PairCodeResponse.deserialize(bytes);
|
||||
}
|
||||
}
|
||||
export class PasskeyResponseRequest extends pb_1.Message {
|
||||
#one_of_decls: number[][] = [];
|
||||
constructor(data?: any[] | {
|
||||
session?: Session;
|
||||
response_json?: string;
|
||||
}) {
|
||||
super();
|
||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
||||
if (!Array.isArray(data) && typeof data == "object") {
|
||||
if ("session" in data && data.session != undefined) {
|
||||
this.session = data.session;
|
||||
}
|
||||
if ("response_json" in data && data.response_json != undefined) {
|
||||
this.response_json = data.response_json;
|
||||
}
|
||||
}
|
||||
}
|
||||
get session() {
|
||||
return pb_1.Message.getWrapperField(this, Session, 1) as Session;
|
||||
}
|
||||
set session(value: Session) {
|
||||
pb_1.Message.setWrapperField(this, 1, value);
|
||||
}
|
||||
get has_session() {
|
||||
return pb_1.Message.getField(this, 1) != null;
|
||||
}
|
||||
get response_json() {
|
||||
return pb_1.Message.getFieldWithDefault(this, 2, "") as string;
|
||||
}
|
||||
set response_json(value: string) {
|
||||
pb_1.Message.setField(this, 2, value);
|
||||
}
|
||||
static fromObject(data: {
|
||||
session?: ReturnType<typeof Session.prototype.toObject>;
|
||||
response_json?: string;
|
||||
}): PasskeyResponseRequest {
|
||||
const message = new PasskeyResponseRequest({});
|
||||
if (data.session != null) {
|
||||
message.session = Session.fromObject(data.session);
|
||||
}
|
||||
if (data.response_json != null) {
|
||||
message.response_json = data.response_json;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
toObject() {
|
||||
const data: {
|
||||
session?: ReturnType<typeof Session.prototype.toObject>;
|
||||
response_json?: string;
|
||||
} = {};
|
||||
if (this.session != null) {
|
||||
data.session = this.session.toObject();
|
||||
}
|
||||
if (this.response_json != null) {
|
||||
data.response_json = this.response_json;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
serialize(): Uint8Array;
|
||||
serialize(w: pb_1.BinaryWriter): void;
|
||||
serialize(w?: pb_1.BinaryWriter): Uint8Array | void {
|
||||
const writer = w || new pb_1.BinaryWriter();
|
||||
if (this.has_session)
|
||||
writer.writeMessage(1, this.session, () => this.session.serialize(writer));
|
||||
if (this.response_json.length)
|
||||
writer.writeString(2, this.response_json);
|
||||
if (!w)
|
||||
return writer.getResultBuffer();
|
||||
}
|
||||
static deserialize(bytes: Uint8Array | pb_1.BinaryReader): PasskeyResponseRequest {
|
||||
const reader = bytes instanceof pb_1.BinaryReader ? bytes : new pb_1.BinaryReader(bytes), message = new PasskeyResponseRequest();
|
||||
while (reader.nextField()) {
|
||||
if (reader.isEndGroup())
|
||||
break;
|
||||
switch (reader.getFieldNumber()) {
|
||||
case 1:
|
||||
reader.readMessage(message.session, () => message.session = Session.deserialize(reader));
|
||||
break;
|
||||
case 2:
|
||||
message.response_json = reader.readString();
|
||||
break;
|
||||
default: reader.skipField();
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
serializeBinary(): Uint8Array {
|
||||
return this.serialize();
|
||||
}
|
||||
static deserializeBinary(bytes: Uint8Array): PasskeyResponseRequest {
|
||||
return PasskeyResponseRequest.deserialize(bytes);
|
||||
}
|
||||
}
|
||||
export class Empty extends pb_1.Message {
|
||||
#one_of_decls: number[][] = [];
|
||||
constructor(data?: any[] | {}) {
|
||||
@@ -11053,6 +11146,24 @@ export namespace messages {
|
||||
responseSerialize: (message: PairCodeResponse) => Buffer.from(message.serialize()),
|
||||
responseDeserialize: (bytes: Buffer) => PairCodeResponse.deserialize(new Uint8Array(bytes))
|
||||
},
|
||||
SubmitPasskeyResponse: {
|
||||
path: "/messages.MessageService/SubmitPasskeyResponse",
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestSerialize: (message: PasskeyResponseRequest) => Buffer.from(message.serialize()),
|
||||
requestDeserialize: (bytes: Buffer) => PasskeyResponseRequest.deserialize(new Uint8Array(bytes)),
|
||||
responseSerialize: (message: Empty) => Buffer.from(message.serialize()),
|
||||
responseDeserialize: (bytes: Buffer) => Empty.deserialize(new Uint8Array(bytes))
|
||||
},
|
||||
ConfirmPasskey: {
|
||||
path: "/messages.MessageService/ConfirmPasskey",
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestSerialize: (message: Session) => Buffer.from(message.serialize()),
|
||||
requestDeserialize: (bytes: Buffer) => Session.deserialize(new Uint8Array(bytes)),
|
||||
responseSerialize: (message: Empty) => Buffer.from(message.serialize()),
|
||||
responseDeserialize: (bytes: Buffer) => Empty.deserialize(new Uint8Array(bytes))
|
||||
},
|
||||
Logout: {
|
||||
path: "/messages.MessageService/Logout",
|
||||
requestStream: false,
|
||||
@@ -11251,6 +11362,15 @@ export namespace messages {
|
||||
responseSerialize: (message: Empty) => Buffer.from(message.serialize()),
|
||||
responseDeserialize: (bytes: Buffer) => Empty.deserialize(new Uint8Array(bytes))
|
||||
},
|
||||
SetGroupMemberAddMode: {
|
||||
path: "/messages.MessageService/SetGroupMemberAddMode",
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestSerialize: (message: JidBoolRequest) => Buffer.from(message.serialize()),
|
||||
requestDeserialize: (bytes: Buffer) => JidBoolRequest.deserialize(new Uint8Array(bytes)),
|
||||
responseSerialize: (message: Empty) => Buffer.from(message.serialize()),
|
||||
responseDeserialize: (bytes: Buffer) => Empty.deserialize(new Uint8Array(bytes))
|
||||
},
|
||||
UpdateGroupParticipants: {
|
||||
path: "/messages.MessageService/UpdateGroupParticipants",
|
||||
requestStream: false,
|
||||
@@ -11599,6 +11719,8 @@ export namespace messages {
|
||||
abstract StopSession(call: grpc_1.ServerUnaryCall<Session, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
|
||||
abstract GetSessionState(call: grpc_1.ServerUnaryCall<Session, SessionStateResponse>, callback: grpc_1.sendUnaryData<SessionStateResponse>): void;
|
||||
abstract RequestCode(call: grpc_1.ServerUnaryCall<PairCodeRequest, PairCodeResponse>, callback: grpc_1.sendUnaryData<PairCodeResponse>): void;
|
||||
abstract SubmitPasskeyResponse(call: grpc_1.ServerUnaryCall<PasskeyResponseRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
|
||||
abstract ConfirmPasskey(call: grpc_1.ServerUnaryCall<Session, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
|
||||
abstract Logout(call: grpc_1.ServerUnaryCall<Session, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
|
||||
abstract SetProfileName(call: grpc_1.ServerUnaryCall<ProfileNameRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
|
||||
abstract SetProfileStatus(call: grpc_1.ServerUnaryCall<ProfileStatusRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
|
||||
@@ -11621,6 +11743,7 @@ export namespace messages {
|
||||
abstract SetGroupPicture(call: grpc_1.ServerUnaryCall<SetPictureRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
|
||||
abstract SetGroupLocked(call: grpc_1.ServerUnaryCall<JidBoolRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
|
||||
abstract SetGroupAnnounce(call: grpc_1.ServerUnaryCall<JidBoolRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
|
||||
abstract SetGroupMemberAddMode(call: grpc_1.ServerUnaryCall<JidBoolRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
|
||||
abstract UpdateGroupParticipants(call: grpc_1.ServerUnaryCall<UpdateParticipantsRequest, JsonList>, callback: grpc_1.sendUnaryData<JsonList>): void;
|
||||
abstract GetProfilePicture(call: grpc_1.ServerUnaryCall<ProfilePictureRequest, ProfilePictureResponse>, callback: grpc_1.sendUnaryData<ProfilePictureResponse>): void;
|
||||
abstract SendPresence(call: grpc_1.ServerUnaryCall<PresenceRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
|
||||
@@ -11676,6 +11799,12 @@ export namespace messages {
|
||||
RequestCode: GrpcUnaryServiceInterface<PairCodeRequest, PairCodeResponse> = (message: PairCodeRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<PairCodeResponse>, options?: grpc_1.CallOptions | grpc_1.requestCallback<PairCodeResponse>, callback?: grpc_1.requestCallback<PairCodeResponse>): grpc_1.ClientUnaryCall => {
|
||||
return super.RequestCode(message, metadata, options, callback);
|
||||
};
|
||||
SubmitPasskeyResponse: GrpcUnaryServiceInterface<PasskeyResponseRequest, Empty> = (message: PasskeyResponseRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<Empty>, options?: grpc_1.CallOptions | grpc_1.requestCallback<Empty>, callback?: grpc_1.requestCallback<Empty>): grpc_1.ClientUnaryCall => {
|
||||
return super.SubmitPasskeyResponse(message, metadata, options, callback);
|
||||
};
|
||||
ConfirmPasskey: GrpcUnaryServiceInterface<Session, Empty> = (message: Session, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<Empty>, options?: grpc_1.CallOptions | grpc_1.requestCallback<Empty>, callback?: grpc_1.requestCallback<Empty>): grpc_1.ClientUnaryCall => {
|
||||
return super.ConfirmPasskey(message, metadata, options, callback);
|
||||
};
|
||||
Logout: GrpcUnaryServiceInterface<Session, Empty> = (message: Session, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<Empty>, options?: grpc_1.CallOptions | grpc_1.requestCallback<Empty>, callback?: grpc_1.requestCallback<Empty>): grpc_1.ClientUnaryCall => {
|
||||
return super.Logout(message, metadata, options, callback);
|
||||
};
|
||||
@@ -11742,6 +11871,9 @@ export namespace messages {
|
||||
SetGroupAnnounce: GrpcUnaryServiceInterface<JidBoolRequest, Empty> = (message: JidBoolRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<Empty>, options?: grpc_1.CallOptions | grpc_1.requestCallback<Empty>, callback?: grpc_1.requestCallback<Empty>): grpc_1.ClientUnaryCall => {
|
||||
return super.SetGroupAnnounce(message, metadata, options, callback);
|
||||
};
|
||||
SetGroupMemberAddMode: GrpcUnaryServiceInterface<JidBoolRequest, Empty> = (message: JidBoolRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<Empty>, options?: grpc_1.CallOptions | grpc_1.requestCallback<Empty>, callback?: grpc_1.requestCallback<Empty>): grpc_1.ClientUnaryCall => {
|
||||
return super.SetGroupMemberAddMode(message, metadata, options, callback);
|
||||
};
|
||||
UpdateGroupParticipants: GrpcUnaryServiceInterface<UpdateParticipantsRequest, JsonList> = (message: UpdateParticipantsRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<JsonList>, options?: grpc_1.CallOptions | grpc_1.requestCallback<JsonList>, callback?: grpc_1.requestCallback<JsonList>): grpc_1.ClientUnaryCall => {
|
||||
return super.UpdateGroupParticipants(message, metadata, options, callback);
|
||||
};
|
||||
|
||||
@@ -488,6 +488,17 @@ function deserialize_messages_PairCodeResponse(buffer_arg) {
|
||||
return gows_pb.PairCodeResponse.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
function serialize_messages_PasskeyResponseRequest(arg) {
|
||||
if (!(arg instanceof gows_pb.PasskeyResponseRequest)) {
|
||||
throw new Error('Expected argument of type messages.PasskeyResponseRequest');
|
||||
}
|
||||
return Buffer.from(arg.serializeBinary());
|
||||
}
|
||||
|
||||
function deserialize_messages_PasskeyResponseRequest(buffer_arg) {
|
||||
return gows_pb.PasskeyResponseRequest.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
function serialize_messages_PresenceRequest(arg) {
|
||||
if (!(arg instanceof gows_pb.PresenceRequest)) {
|
||||
throw new Error('Expected argument of type messages.PresenceRequest');
|
||||
@@ -764,6 +775,28 @@ startSession: {
|
||||
responseSerialize: serialize_messages_PairCodeResponse,
|
||||
responseDeserialize: deserialize_messages_PairCodeResponse,
|
||||
},
|
||||
submitPasskeyResponse: {
|
||||
path: '/messages.MessageService/SubmitPasskeyResponse',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestType: gows_pb.PasskeyResponseRequest,
|
||||
responseType: gows_pb.Empty,
|
||||
requestSerialize: serialize_messages_PasskeyResponseRequest,
|
||||
requestDeserialize: deserialize_messages_PasskeyResponseRequest,
|
||||
responseSerialize: serialize_messages_Empty,
|
||||
responseDeserialize: deserialize_messages_Empty,
|
||||
},
|
||||
confirmPasskey: {
|
||||
path: '/messages.MessageService/ConfirmPasskey',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestType: gows_pb.Session,
|
||||
responseType: gows_pb.Empty,
|
||||
requestSerialize: serialize_messages_Session,
|
||||
requestDeserialize: deserialize_messages_Session,
|
||||
responseSerialize: serialize_messages_Empty,
|
||||
responseDeserialize: deserialize_messages_Empty,
|
||||
},
|
||||
logout: {
|
||||
path: '/messages.MessageService/Logout',
|
||||
requestStream: false,
|
||||
@@ -1017,6 +1050,18 @@ setGroupAnnounce: {
|
||||
responseDeserialize: deserialize_messages_Empty,
|
||||
},
|
||||
// send messages only by admins
|
||||
setGroupMemberAddMode: {
|
||||
path: '/messages.MessageService/SetGroupMemberAddMode',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestType: gows_pb.JidBoolRequest,
|
||||
responseType: gows_pb.Empty,
|
||||
requestSerialize: serialize_messages_JidBoolRequest,
|
||||
requestDeserialize: deserialize_messages_JidBoolRequest,
|
||||
responseSerialize: serialize_messages_Empty,
|
||||
responseDeserialize: deserialize_messages_Empty,
|
||||
},
|
||||
// who can add members - true - all members, false - admins only
|
||||
updateGroupParticipants: {
|
||||
path: '/messages.MessageService/UpdateGroupParticipants',
|
||||
requestStream: false,
|
||||
|
||||
@@ -84,6 +84,7 @@ goog.exportSymbol('proto.messages.Pagination', null, global);
|
||||
goog.exportSymbol('proto.messages.PairCodeRequest', null, global);
|
||||
goog.exportSymbol('proto.messages.PairCodeResponse', null, global);
|
||||
goog.exportSymbol('proto.messages.ParticipantAction', null, global);
|
||||
goog.exportSymbol('proto.messages.PasskeyResponseRequest', null, global);
|
||||
goog.exportSymbol('proto.messages.PhoneInfo', null, global);
|
||||
goog.exportSymbol('proto.messages.PollMessage', null, global);
|
||||
goog.exportSymbol('proto.messages.PollVoteMessage', null, global);
|
||||
@@ -395,6 +396,27 @@ if (goog.DEBUG && !COMPILED) {
|
||||
*/
|
||||
proto.messages.PairCodeResponse.displayName = 'proto.messages.PairCodeResponse';
|
||||
}
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||
* server response, or constructed directly in Javascript. The array is used
|
||||
* in place and becomes part of the constructed object. It is not cloned.
|
||||
* If no data is provided, the constructed object will be empty, but still
|
||||
* valid.
|
||||
* @extends {jspb.Message}
|
||||
* @constructor
|
||||
*/
|
||||
proto.messages.PasskeyResponseRequest = function(opt_data) {
|
||||
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
|
||||
};
|
||||
goog.inherits(proto.messages.PasskeyResponseRequest, jspb.Message);
|
||||
if (goog.DEBUG && !COMPILED) {
|
||||
/**
|
||||
* @public
|
||||
* @override
|
||||
*/
|
||||
proto.messages.PasskeyResponseRequest.displayName = 'proto.messages.PasskeyResponseRequest';
|
||||
}
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||
@@ -4190,6 +4212,187 @@ proto.messages.PairCodeResponse.prototype.setCode = function(value) {
|
||||
|
||||
|
||||
|
||||
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||
/**
|
||||
* Creates an object representation of this proto.
|
||||
* Field names that are reserved in JavaScript and will be renamed to pb_name.
|
||||
* Optional fields that are not set will be set to undefined.
|
||||
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
|
||||
* For the list of reserved names please see:
|
||||
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
|
||||
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
|
||||
* JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @return {!Object}
|
||||
*/
|
||||
proto.messages.PasskeyResponseRequest.prototype.toObject = function(opt_includeInstance) {
|
||||
return proto.messages.PasskeyResponseRequest.toObject(opt_includeInstance, this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Static version of the {@see toObject} method.
|
||||
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
|
||||
* the JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @param {!proto.messages.PasskeyResponseRequest} msg The msg instance to transform.
|
||||
* @return {!Object}
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.messages.PasskeyResponseRequest.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
session: (f = msg.getSession()) && proto.messages.Session.toObject(includeInstance, f),
|
||||
responseJson: jspb.Message.getFieldWithDefault(msg, 2, "")
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
obj.$jspbMessageInstance = msg;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format).
|
||||
* @param {jspb.ByteSource} bytes The bytes to deserialize.
|
||||
* @return {!proto.messages.PasskeyResponseRequest}
|
||||
*/
|
||||
proto.messages.PasskeyResponseRequest.deserializeBinary = function(bytes) {
|
||||
var reader = new jspb.BinaryReader(bytes);
|
||||
var msg = new proto.messages.PasskeyResponseRequest;
|
||||
return proto.messages.PasskeyResponseRequest.deserializeBinaryFromReader(msg, reader);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format) from the
|
||||
* given reader into the given message object.
|
||||
* @param {!proto.messages.PasskeyResponseRequest} msg The message object to deserialize into.
|
||||
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
|
||||
* @return {!proto.messages.PasskeyResponseRequest}
|
||||
*/
|
||||
proto.messages.PasskeyResponseRequest.deserializeBinaryFromReader = function(msg, reader) {
|
||||
while (reader.nextField()) {
|
||||
if (reader.isEndGroup()) {
|
||||
break;
|
||||
}
|
||||
var field = reader.getFieldNumber();
|
||||
switch (field) {
|
||||
case 1:
|
||||
var value = new proto.messages.Session;
|
||||
reader.readMessage(value,proto.messages.Session.deserializeBinaryFromReader);
|
||||
msg.setSession(value);
|
||||
break;
|
||||
case 2:
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.setResponseJson(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the message to binary data (in protobuf wire format).
|
||||
* @return {!Uint8Array}
|
||||
*/
|
||||
proto.messages.PasskeyResponseRequest.prototype.serializeBinary = function() {
|
||||
var writer = new jspb.BinaryWriter();
|
||||
proto.messages.PasskeyResponseRequest.serializeBinaryToWriter(this, writer);
|
||||
return writer.getResultBuffer();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the given message to binary data (in protobuf wire
|
||||
* format), writing to the given BinaryWriter.
|
||||
* @param {!proto.messages.PasskeyResponseRequest} message
|
||||
* @param {!jspb.BinaryWriter} writer
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.messages.PasskeyResponseRequest.serializeBinaryToWriter = function(message, writer) {
|
||||
var f = undefined;
|
||||
f = message.getSession();
|
||||
if (f != null) {
|
||||
writer.writeMessage(
|
||||
1,
|
||||
f,
|
||||
proto.messages.Session.serializeBinaryToWriter
|
||||
);
|
||||
}
|
||||
f = message.getResponseJson();
|
||||
if (f.length > 0) {
|
||||
writer.writeString(
|
||||
2,
|
||||
f
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional Session session = 1;
|
||||
* @return {?proto.messages.Session}
|
||||
*/
|
||||
proto.messages.PasskeyResponseRequest.prototype.getSession = function() {
|
||||
return /** @type{?proto.messages.Session} */ (
|
||||
jspb.Message.getWrapperField(this, proto.messages.Session, 1));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {?proto.messages.Session|undefined} value
|
||||
* @return {!proto.messages.PasskeyResponseRequest} returns this
|
||||
*/
|
||||
proto.messages.PasskeyResponseRequest.prototype.setSession = function(value) {
|
||||
return jspb.Message.setWrapperField(this, 1, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the message field making it undefined.
|
||||
* @return {!proto.messages.PasskeyResponseRequest} returns this
|
||||
*/
|
||||
proto.messages.PasskeyResponseRequest.prototype.clearSession = function() {
|
||||
return this.setSession(undefined);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether this field is set.
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.messages.PasskeyResponseRequest.prototype.hasSession = function() {
|
||||
return jspb.Message.getField(this, 1) != null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional string response_json = 2;
|
||||
* @return {string}
|
||||
*/
|
||||
proto.messages.PasskeyResponseRequest.prototype.getResponseJson = function() {
|
||||
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @return {!proto.messages.PasskeyResponseRequest} returns this
|
||||
*/
|
||||
proto.messages.PasskeyResponseRequest.prototype.setResponseJson = function(value) {
|
||||
return jspb.Message.setProto3StringField(this, 2, value);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||
/**
|
||||
* Creates an object representation of this proto.
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { parseGowsReachoutTimelock } from '@waha/core/engines/gows/reachouttimelock';
|
||||
|
||||
describe('parseGowsReachoutTimelock', () => {
|
||||
it('parses a full payload', () => {
|
||||
const timelock = parseGowsReachoutTimelock({
|
||||
enforcement_type: 'RESTRICT_ALL_COMPANIONS',
|
||||
is_active: true,
|
||||
time_enforcement_ends: '1784477333',
|
||||
});
|
||||
expect(timelock).toEqual({
|
||||
enforcementType: 'RESTRICT_ALL_COMPANIONS',
|
||||
isActive: true,
|
||||
timeEnforcementEnds: 1784477333,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats absent is_active as inactive (omitempty)', () => {
|
||||
const timelock = parseGowsReachoutTimelock({
|
||||
enforcement_type: 'RESTRICT_ALL_COMPANIONS',
|
||||
});
|
||||
expect(timelock.isActive).toBe(false);
|
||||
});
|
||||
|
||||
it('treats absent time_enforcement_ends as null (omitzero)', () => {
|
||||
const timelock = parseGowsReachoutTimelock({
|
||||
enforcement_type: 'RESTRICT_ALL_COMPANIONS',
|
||||
is_active: true,
|
||||
});
|
||||
expect(timelock.timeEnforcementEnds).toBeNull();
|
||||
});
|
||||
|
||||
it('defaults enforcement type on an empty payload', () => {
|
||||
const timelock = parseGowsReachoutTimelock({});
|
||||
expect(timelock).toEqual({
|
||||
enforcementType: 'DEFAULT',
|
||||
isActive: false,
|
||||
timeEnforcementEnds: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null for a non numeric time_enforcement_ends', () => {
|
||||
const timelock = parseGowsReachoutTimelock({
|
||||
is_active: true,
|
||||
time_enforcement_ends: 'not-a-number',
|
||||
});
|
||||
expect(timelock.timeEnforcementEnds).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
ReachoutTimelockData,
|
||||
ReachoutTimelockEnforcementType,
|
||||
} from '@waha/structures/sessions.dto';
|
||||
import { EnsureSeconds } from '@waha/utils/timehelper';
|
||||
|
||||
/**
|
||||
* Normalize whatsmeow's events.NotifyAccountReachoutTimelock payload.
|
||||
*/
|
||||
export function parseGowsReachoutTimelock(data: any): ReachoutTimelockData {
|
||||
// 'time_enforcement_ends' is a unix seconds string, absent when zero (Go's omitzero)
|
||||
let timeEnforcementEnds: number | null = null;
|
||||
if (data?.time_enforcement_ends) {
|
||||
const parsed = parseInt(data.time_enforcement_ends, 10);
|
||||
timeEnforcementEnds = Number.isNaN(parsed) ? null : EnsureSeconds(parsed);
|
||||
}
|
||||
const enforcementType =
|
||||
data?.enforcement_type ?? ReachoutTimelockEnforcementType.DEFAULT;
|
||||
return {
|
||||
enforcementType: enforcementType as ReachoutTimelockEnforcementType,
|
||||
// 'is_active' is absent when false (Go's omitempty)
|
||||
isActive: data?.is_active === true,
|
||||
timeEnforcementEnds: timeEnforcementEnds,
|
||||
};
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
parseJsonList,
|
||||
statusToAck,
|
||||
} from '@waha/core/engines/gows/helpers';
|
||||
import { parseGowsReachoutTimelock } from '@waha/core/engines/gows/reachouttimelock';
|
||||
import { GowsAuthFactoryCore } from '@waha/core/engines/gows/store/GowsAuthFactoryCore';
|
||||
import {
|
||||
extractBody,
|
||||
@@ -48,6 +49,10 @@ import {
|
||||
toCusFormat,
|
||||
toJID,
|
||||
} from '@waha/core/utils/jids';
|
||||
import {
|
||||
PasskeyChallenge,
|
||||
PasskeyConfirmationResponse,
|
||||
} from '@waha/structures/auth.dto';
|
||||
import {
|
||||
Channel,
|
||||
ChannelListResult,
|
||||
@@ -114,6 +119,7 @@ import {
|
||||
GroupSortField,
|
||||
Participant,
|
||||
ParticipantsRequest,
|
||||
SettingsMemberAddMode,
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '@waha/structures/groups.dto';
|
||||
import { ReplyToMessage } from '@waha/structures/message.dto';
|
||||
@@ -231,6 +237,7 @@ enum WhatsMeowEvent {
|
||||
CHAT_PRESENCE = 'events.ChatPresence',
|
||||
PUSH_NAME_SETTING = 'events.PushNameSetting',
|
||||
LOGGED_OUT = 'events.LoggedOut',
|
||||
NOTIFY_ACCOUNT_REACHOUT_TIMELOCK = 'events.NotifyAccountReachoutTimelock',
|
||||
// Groups
|
||||
GROUP_INFO = 'events.GroupInfo',
|
||||
JOINED_GROUP = 'events.JoinedGroup',
|
||||
@@ -419,6 +426,25 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
if (data.Event == 'success') {
|
||||
return;
|
||||
}
|
||||
if (data.Event == 'passkey-request') {
|
||||
// WhatsApp requires a passkey (WebAuthn) to finish pairing this account.
|
||||
const challenge = data.PasskeyRequest?.PublicKey ?? null;
|
||||
this.logger.info('Passkey required to finish pairing');
|
||||
this.setStatus(WAHASessionStatus.PASSKEY_REQUIRED, challenge);
|
||||
return;
|
||||
}
|
||||
if (data.Event == 'passkey-confirmation') {
|
||||
// Only the manual case reaches us - when WhatsApp allows skipping the
|
||||
// handoff UX, whatsmeow confirms on its own and emits nothing.
|
||||
// The operator must see the code, verify it matches the one shown on
|
||||
// their phone, then confirm via POST .../auth/passkey/confirm.
|
||||
const code = data.PasskeyConfirmation?.Code ?? null;
|
||||
this.logger.info({ code: code }, 'Passkey confirmation code');
|
||||
this.setStatus(WAHASessionStatus.PASSKEY_CONFIRMATION_REQUIRED, {
|
||||
code: code,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (data.Event != 'code') {
|
||||
this.logger.warn(data, 'Failed QR item event');
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
@@ -430,6 +456,17 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
}
|
||||
this.qr.save(qr);
|
||||
this.printQR(this.qr);
|
||||
if (
|
||||
this.status === WAHASessionStatus.PASSKEY_REQUIRED ||
|
||||
this.status === WAHASessionStatus.PASSKEY_CONFIRMATION_REQUIRED
|
||||
) {
|
||||
// The underlying whatsmeow QR rotation keeps emitting fresh codes in
|
||||
// parallel while the passkey challenge is pending (it doesn't know
|
||||
// about the passkey step). Don't let that bounce the session back to
|
||||
// SCAN_QR_CODE mid-flow — the operator is busy signing the passkey.
|
||||
// It'd also wipe the passkey data off the status.
|
||||
return;
|
||||
}
|
||||
this.status = WAHASessionStatus.SCAN_QR_CODE;
|
||||
});
|
||||
events.on(WhatsMeowEvent.PUSH_NAME_SETTING, (data) => {
|
||||
@@ -439,6 +476,9 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
this.logger.error('Logged out');
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
});
|
||||
events.on(WhatsMeowEvent.NOTIFY_ACCOUNT_REACHOUT_TIMELOCK, (data) => {
|
||||
this.reachoutTimelock.update(parseGowsReachoutTimelock(data));
|
||||
});
|
||||
events.on(WhatsMeowEvent.PRESENCE, (event: gows.Presence) => {
|
||||
if (isJidGroup(event.From)) {
|
||||
// So group is not "online"
|
||||
@@ -849,12 +889,45 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
return { code: code };
|
||||
}
|
||||
|
||||
public async sendPasskeyResponse(responseJson: string): Promise<void> {
|
||||
const request = new messages.PasskeyResponseRequest({
|
||||
session: this.session,
|
||||
response_json: responseJson,
|
||||
});
|
||||
await promisify(this.client.SubmitPasskeyResponse)(request);
|
||||
}
|
||||
|
||||
public async confirmPasskey(): Promise<void> {
|
||||
await promisify(this.client.ConfirmPasskey)(this.session);
|
||||
}
|
||||
|
||||
public getPasskeyChallenge(): PasskeyChallenge {
|
||||
if (this.status !== WAHASessionStatus.PASSKEY_REQUIRED) {
|
||||
throw new UnprocessableEntityException(
|
||||
'No passkey challenge is pending for the session',
|
||||
);
|
||||
}
|
||||
return this.statusData;
|
||||
}
|
||||
|
||||
public getPasskeyConfirmation(): PasskeyConfirmationResponse {
|
||||
if (this.status !== WAHASessionStatus.PASSKEY_CONFIRMATION_REQUIRED) {
|
||||
throw new UnprocessableEntityException(
|
||||
'No passkey confirmation is pending for the session',
|
||||
);
|
||||
}
|
||||
return { code: this.statusData?.code };
|
||||
}
|
||||
|
||||
async unpair() {
|
||||
await promisify(this.client.Logout)(this.session);
|
||||
}
|
||||
|
||||
public getSessionMeInfo(): MeInfo | null {
|
||||
return this.me;
|
||||
if (!this.me) {
|
||||
return null;
|
||||
}
|
||||
return { ...this.me, reachoutTimelock: this.reachoutTimelock.value };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1578,6 +1651,24 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
return;
|
||||
}
|
||||
|
||||
public async getMemberAddMode(id): Promise<SettingsMemberAddMode> {
|
||||
const group = await this.getGroup(id);
|
||||
return {
|
||||
membersCanAddNewMember: group.MemberAddMode === 'all_member_add',
|
||||
};
|
||||
}
|
||||
|
||||
@Activity()
|
||||
public async setMemberAddMode(id, value) {
|
||||
const req = new messages.JidBoolRequest({
|
||||
session: this.session,
|
||||
jid: id,
|
||||
value: value,
|
||||
});
|
||||
await promisify(this.client.SetGroupMemberAddMode)(req);
|
||||
return;
|
||||
}
|
||||
|
||||
public deleteGroup(id) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
@@ -2958,6 +3049,19 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* gRPC status codes returned by GOWS DownloadMedia that represent a definitive
|
||||
* failure (the media cannot be fetched right now and retrying won't help). Used
|
||||
* to tag the error as non-retriable so MediaManager doesn't re-issue the call.
|
||||
*/
|
||||
const NON_RETRIABLE_DOWNLOAD_MEDIA_CODES: Set<number> = new Set([
|
||||
grpc.status.FAILED_PRECONDITION,
|
||||
grpc.status.NOT_FOUND,
|
||||
grpc.status.INVALID_ARGUMENT,
|
||||
grpc.status.PERMISSION_DENIED,
|
||||
grpc.status.UNIMPLEMENTED,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Many encrypted stickers carry URL "https://a.whatsapp.net" with no path. If
|
||||
* that string is passed to DownloadMedia, the Go client may attempt HTTP GET to
|
||||
@@ -3062,6 +3166,12 @@ export class GOWSEngineMediaProcessor implements IMediaEngineProcessor<any> {
|
||||
} catch (err) {
|
||||
if (err?.code === grpc.status.DEADLINE_EXCEEDED) {
|
||||
err.message = `DownloadMedia timed out after ${mediaDownloadTimeoutMs}ms for message '${message?.Info?.ID}'`;
|
||||
} else if (NON_RETRIABLE_DOWNLOAD_MEDIA_CODES.has(err?.code)) {
|
||||
// The media is not currently downloadable (e.g. CDN 403 after the
|
||||
// anonymous + media-retry fallbacks, or the object is gone). Retrying
|
||||
// the gRPC call won't help and each attempt can block on a media-retry
|
||||
// wait, so mark it so MediaManager stops retrying.
|
||||
err.nonRetriable = true;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import makeWASocket, {
|
||||
WAMessageContent,
|
||||
WAMessageKey,
|
||||
WAMessageUpdate,
|
||||
WAVersion,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { WACallEvent } from '@adiwajshing/baileys/lib/Types/Call';
|
||||
import { BaileysEventMap } from '@adiwajshing/baileys/lib/Types/Events';
|
||||
@@ -73,7 +74,12 @@ import { AckToStatus, StatusToAck } from '@waha/core/utils/acks';
|
||||
import { pairs } from '@waha/utils/pairs';
|
||||
import { ExtractMessageKeysForRead } from '@waha/core/utils/convertors';
|
||||
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
|
||||
import { isJidNewsletter, toCusFormat, toJID } from '@waha/core/utils/jids';
|
||||
import {
|
||||
isJidNewsletter,
|
||||
jidsFromKey,
|
||||
toCusFormat,
|
||||
toJID,
|
||||
} from '@waha/core/utils/jids';
|
||||
import { DistinctAck, DistinctMessages } from '@waha/core/utils/reactive';
|
||||
import {
|
||||
flipObject,
|
||||
@@ -149,6 +155,7 @@ import {
|
||||
CreateGroupRequest,
|
||||
GroupParticipant,
|
||||
ParticipantsRequest,
|
||||
SettingsMemberAddMode,
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '@waha/structures/groups.dto';
|
||||
import {
|
||||
@@ -166,7 +173,11 @@ import {
|
||||
WAHAPresenceData,
|
||||
} from '@waha/structures/presence.dto';
|
||||
import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
|
||||
import { MeInfo } from '@waha/structures/sessions.dto';
|
||||
import {
|
||||
MeInfo,
|
||||
ReachoutTimelockEnforcementType,
|
||||
} from '@waha/structures/sessions.dto';
|
||||
import { EnsureSeconds } from '@waha/utils/timehelper';
|
||||
import {
|
||||
BROADCAST_ID,
|
||||
DeleteStatusRequest,
|
||||
@@ -235,6 +246,7 @@ import { detectMimetype } from '@waha/utils/files';
|
||||
import esm from '@waha/vendor/esm';
|
||||
import axios from 'axios';
|
||||
import axiosRetry from 'axios-retry';
|
||||
import { formatWaVersion } from '@waha/core/engines/noweb/waversion';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const promiseRetry = require('promise-retry');
|
||||
|
||||
@@ -259,11 +271,16 @@ const PresenceStatuses = {
|
||||
};
|
||||
const ToEnginePresenceStatus = flipObject(PresenceStatuses);
|
||||
|
||||
export interface NowebConfig {
|
||||
waVersion: WAVersion;
|
||||
}
|
||||
|
||||
export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
private START_ATTEMPT_DELAY_SECONDS = 2;
|
||||
private AUTO_RESTART_AFTER_SECONDS = 28 * 60;
|
||||
|
||||
engine = WAHAEngine.NOWEB;
|
||||
protected engineConfig: NowebConfig;
|
||||
authFactory = new NowebAuthFactoryCore();
|
||||
storageFactory = new NowebStorageFactoryCore();
|
||||
private startDelayedJob: SingleDelayedJobRunner;
|
||||
@@ -381,7 +398,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
if (markOnlineOnConnect == undefined) {
|
||||
markOnlineOnConnect = true;
|
||||
}
|
||||
const version = this.engineConfig?.waVersion;
|
||||
return {
|
||||
version: version,
|
||||
agent: agents?.socket,
|
||||
// Baileys media upload uses Node https.request in Node runtime.
|
||||
fetchAgent: agents?.fetch as Agent,
|
||||
@@ -419,6 +438,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
agents,
|
||||
state,
|
||||
) as SocketConfig;
|
||||
this.logger.info(
|
||||
`Connecting using wa.version = ${formatWaVersion(socketConfig.version)}`,
|
||||
);
|
||||
const sock = makeWASocket(socketConfig);
|
||||
sock.ev.on('creds.update', saveCreds);
|
||||
return sock;
|
||||
@@ -542,11 +564,32 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
this.logger.debug(`Start listening ${BaileysEvents.CONNECTION_UPDATE}...`);
|
||||
this.sock.ev.on('connection.update', async (update) => {
|
||||
const { connection, lastDisconnect, qr, isNewLogin } = update;
|
||||
if (update.reachoutTimeLock) {
|
||||
const timelock = update.reachoutTimeLock;
|
||||
const enforcementType =
|
||||
timelock.enforcementType ?? ReachoutTimelockEnforcementType.DEFAULT;
|
||||
let timeEnforcementEnds: number | null = null;
|
||||
if (timelock.timeEnforcementEnds) {
|
||||
timeEnforcementEnds = EnsureSeconds(
|
||||
timelock.timeEnforcementEnds.getTime(),
|
||||
);
|
||||
}
|
||||
this.reachoutTimelock.update({
|
||||
enforcementType: enforcementType as ReachoutTimelockEnforcementType,
|
||||
isActive: timelock.isActive === true,
|
||||
timeEnforcementEnds: timeEnforcementEnds,
|
||||
});
|
||||
}
|
||||
if (isNewLogin) {
|
||||
this.restartClient();
|
||||
} else if (connection === 'open') {
|
||||
this.qr.save('');
|
||||
this.status = WAHASessionStatus.WORKING;
|
||||
// Ask WhatsApp for the current reachout timelock state, so a restarted session learns about
|
||||
// an ongoing timelock without waiting for a push. The result arrives via 'connection.update'
|
||||
this.sock?.fetchAccountReachoutTimelock?.().catch((error) => {
|
||||
this.logger.warn(`Failed to fetch reachout timelock: ${error}`);
|
||||
});
|
||||
// Do we need to resubscribe?
|
||||
// Ideally not, we need to explicitly call interesting
|
||||
// jids every 1 minute
|
||||
@@ -849,6 +892,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
id: toCusFormat(meId),
|
||||
pushName: me.name,
|
||||
lid: jidNormalizedUser(me.lid),
|
||||
reachoutTimelock: this.reachoutTimelock.value,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1205,12 +1249,20 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
urlInfo.jpegThumbnail = thumbnail.buffer;
|
||||
} else {
|
||||
// upload HQ thumbnail
|
||||
// newsletters need the thumbnail uploaded unencrypted (plaintext path),
|
||||
// otherwise clients can not render it (white/blurred preview image)
|
||||
const uploadToServer = this.sock.waUploadToServer;
|
||||
const uploadThumbnail = async (encFilePath, opts) => {
|
||||
opts.newsletter = isJidNewsletter(chatId);
|
||||
return await uploadToServer(encFilePath, opts);
|
||||
};
|
||||
const { imageMessage } = await esm.b.prepareWAMessageMedia(
|
||||
{ image: content },
|
||||
{
|
||||
upload: this.sock.waUploadToServer,
|
||||
upload: uploadThumbnail,
|
||||
mediaTypeOverride: 'thumbnail-link',
|
||||
options: { signal: AbortSignal.timeout(10_000) },
|
||||
jid: chatId,
|
||||
},
|
||||
);
|
||||
urlInfo.jpegThumbnail = imageMessage?.jpegThumbnail
|
||||
@@ -1936,6 +1988,17 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
return await this.sock.groupSettingUpdate(id, setting);
|
||||
}
|
||||
|
||||
public async getMemberAddMode(id): Promise<SettingsMemberAddMode> {
|
||||
const group = await this.getGroup(id);
|
||||
return { membersCanAddNewMember: group.memberAddMode };
|
||||
}
|
||||
|
||||
@Activity()
|
||||
public async setMemberAddMode(id, value) {
|
||||
const mode = value ? 'all_member_add' : 'admin_add';
|
||||
return await this.sock.groupMemberAddMode(id, mode);
|
||||
}
|
||||
|
||||
@Activity()
|
||||
public async leaveGroup(id) {
|
||||
return this.sock.groupLeave(id);
|
||||
@@ -2882,9 +2945,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
if (!origMsgId) {
|
||||
return '';
|
||||
}
|
||||
const jidsToTry = [targetKey.remoteJid, editMessage.key?.remoteJid].filter(
|
||||
Boolean,
|
||||
);
|
||||
const editKey = editMessage.key as WAMessageKey | undefined;
|
||||
const jidsToTry = [
|
||||
targetKey.remoteJid,
|
||||
editKey?.remoteJid,
|
||||
editKey?.remoteJidAlt,
|
||||
].filter(Boolean);
|
||||
let stored: proto.IWebMessageInfo | undefined;
|
||||
for (const jid of jidsToTry) {
|
||||
stored = await this.store?.loadMessage(jid, origMsgId);
|
||||
@@ -2915,40 +2981,75 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
if (!encPayload || !encIv) {
|
||||
return '';
|
||||
}
|
||||
const editInfo = {
|
||||
Chat: editMessage.key?.remoteJid,
|
||||
Sender:
|
||||
editMessage.key?.participant ||
|
||||
(editMessage.key?.fromMe ? undefined : editMessage.key?.remoteJid),
|
||||
};
|
||||
const modificationSenderJid = jidToNonAD(editInfo.Sender || '');
|
||||
const primaryOrig = getOrigSenderJidForMsgSecret(editInfo, {
|
||||
fromMe: targetKey.fromMe,
|
||||
remoteJID: targetKey.remoteJid,
|
||||
participant: targetKey.participant,
|
||||
});
|
||||
const candidates: string[] = [primaryOrig];
|
||||
|
||||
// The editor ("modification sender").
|
||||
let modificationSenderJids: Array<string | null | undefined>;
|
||||
if (editKey?.fromMe) {
|
||||
// Try both user lid and c.us
|
||||
modificationSenderJids = [this.sock?.user?.lid, this.sock?.user?.id];
|
||||
} else {
|
||||
const editJids = editKey ? jidsFromKey(editKey) : null;
|
||||
modificationSenderJids = [
|
||||
editKey?.participant || editKey?.remoteJid,
|
||||
editJids?.lid,
|
||||
editJids?.pn,
|
||||
];
|
||||
}
|
||||
modificationSenderJids = lodash
|
||||
.chain(modificationSenderJids)
|
||||
.filter(Boolean)
|
||||
.map(jidToNonAD)
|
||||
.uniq()
|
||||
.value();
|
||||
if (modificationSenderJids.length === 0) {
|
||||
modificationSenderJids.push('');
|
||||
}
|
||||
|
||||
const remoteNonAD = targetKey.remoteJid
|
||||
? jidToNonAD(targetKey.remoteJid)
|
||||
: '';
|
||||
if (remoteNonAD && !candidates.includes(remoteNonAD)) {
|
||||
candidates.push(remoteNonAD);
|
||||
}
|
||||
const participantNonAD = targetKey.participant
|
||||
? jidToNonAD(targetKey.participant)
|
||||
: '';
|
||||
if (participantNonAD && !candidates.includes(participantNonAD)) {
|
||||
candidates.push(participantNonAD);
|
||||
|
||||
// Map dedupes by key and keeps the first-insertion order
|
||||
const attempts = new Map<
|
||||
string,
|
||||
{ origSenderJid: string; modificationSenderJid: string }
|
||||
>();
|
||||
for (const modificationSenderJid of modificationSenderJids) {
|
||||
const primaryOrigSenderJid = getOrigSenderJidForMsgSecret(
|
||||
{ Chat: editKey?.remoteJid, Sender: modificationSenderJid },
|
||||
{
|
||||
fromMe: targetKey.fromMe,
|
||||
remoteJID: targetKey.remoteJid,
|
||||
participant: targetKey.participant,
|
||||
},
|
||||
);
|
||||
const origSenderJids = [
|
||||
primaryOrigSenderJid,
|
||||
remoteNonAD,
|
||||
participantNonAD,
|
||||
].filter(Boolean);
|
||||
for (const origSenderJid of origSenderJids) {
|
||||
// Avoid duplicates in attemps
|
||||
const attemptKey = `${origSenderJid}|${modificationSenderJid}`;
|
||||
attempts.set(attemptKey, {
|
||||
origSenderJid: origSenderJid,
|
||||
modificationSenderJid: modificationSenderJid,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let lastErr: unknown;
|
||||
for (const origSenderJid of candidates) {
|
||||
for (const attempt of attempts.values()) {
|
||||
try {
|
||||
const decoded = decryptSecretEncryptedMessageEditProto({
|
||||
encPayload: encPayload,
|
||||
encIv: encIv,
|
||||
origMsgId: origMsgId,
|
||||
origSenderJid: origSenderJid,
|
||||
modificationSenderJid: modificationSenderJid,
|
||||
origSenderJid: attempt.origSenderJid,
|
||||
modificationSenderJid: attempt.modificationSenderJid,
|
||||
origMsgSecret: origSecret,
|
||||
});
|
||||
const text = extractBody(decoded) || '';
|
||||
@@ -2960,7 +3061,11 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
}
|
||||
this.logger.debug(
|
||||
{ err: lastErr, origMsgId: origMsgId, candidates: candidates },
|
||||
{
|
||||
err: lastErr,
|
||||
origMsgId: origMsgId,
|
||||
attempts: Array.from(attempts.keys()),
|
||||
},
|
||||
'NOWEB message edit decrypt: AES-GCM or protobuf decode failed',
|
||||
);
|
||||
return '';
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
formatWaVersion,
|
||||
isWaVersionHigher,
|
||||
parseWaVersion,
|
||||
} from '@waha/core/engines/noweb/waversion';
|
||||
|
||||
describe('parseWaVersion', () => {
|
||||
it('parses a valid version', () => {
|
||||
expect(parseWaVersion('2.3000.1043857760')).toEqual([2, 3000, 1043857760]);
|
||||
});
|
||||
|
||||
it('trims whitespace', () => {
|
||||
expect(parseWaVersion(' 2.3000.1 ')).toEqual([2, 3000, 1]);
|
||||
});
|
||||
|
||||
it.each(['', '2.3000', '2.3000.1.2', '2.3000.abc', 'latest'])(
|
||||
"returns null for invalid value '%s'",
|
||||
(value) => {
|
||||
expect(parseWaVersion(value)).toBeNull();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('formatWaVersion', () => {
|
||||
it('formats a version', () => {
|
||||
expect(formatWaVersion([2, 3000, 1043857760])).toBe('2.3000.1043857760');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isWaVersionHigher', () => {
|
||||
it('compares by the last part', () => {
|
||||
expect(isWaVersionHigher([2, 3000, 2], [2, 3000, 1])).toBe(true);
|
||||
expect(isWaVersionHigher([2, 3000, 1], [2, 3000, 2])).toBe(false);
|
||||
});
|
||||
|
||||
it('compares by the middle part first', () => {
|
||||
expect(isWaVersionHigher([2, 3001, 1], [2, 3000, 999])).toBe(true);
|
||||
expect(isWaVersionHigher([2, 3000, 999], [2, 3001, 1])).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for equal versions', () => {
|
||||
expect(isWaVersionHigher([2, 3000, 1], [2, 3000, 1])).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { WAVersion } from '@adiwajshing/baileys';
|
||||
|
||||
export function parseWaVersion(value: string): WAVersion | null {
|
||||
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value.trim());
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
||||
}
|
||||
|
||||
export function formatWaVersion(version: WAVersion): string {
|
||||
return version.join('.');
|
||||
}
|
||||
|
||||
export function isWaVersionHigher(left: WAVersion, right: WAVersion): boolean {
|
||||
for (let index = 0; index < 3; index++) {
|
||||
if (left[index] !== right[index]) {
|
||||
return left[index] > right[index];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { WebJSPresence } from '@waha/core/engines/webjs/types';
|
||||
import { GetSerialized } from '@waha/core/utils/serialized';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { Label } from '@waha/structures/labels.dto';
|
||||
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
|
||||
@@ -14,18 +15,12 @@ import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
|
||||
|
||||
import { CallErrorEvent, PAGE_CALL_ERROR_EVENT, WPage } from './WPage';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { LoadWAHA } = require('./_WAHA.js');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { LoadLodash } = require('./_lodash.js');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { LoadPaginator } = require('./_Paginator.js');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { LoadWAHACore } = require('./_WAHACore.js');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory');
|
||||
|
||||
@@ -81,7 +76,7 @@ function extractReactionsByMessageKey(
|
||||
): Map<string, ChannelMessageReaction[]> {
|
||||
const reactions = new Map();
|
||||
for (const reaction of newsletterReactions) {
|
||||
const key = reaction.parentMsgKey._serialized;
|
||||
const key = GetSerialized(reaction.parentMsgKey);
|
||||
const emojiCountMap = reaction.emojiCountMap;
|
||||
const reactionList: ChannelMessageReaction[] = [];
|
||||
for (const emoji in emojiCountMap) {
|
||||
@@ -130,8 +125,6 @@ export class WebjsClientCore extends Client {
|
||||
async injectWaha() {
|
||||
await this.pupPage.evaluate(LoadLodash);
|
||||
await this.pupPage.evaluate(LoadPaginator);
|
||||
await this.pupPage.evaluate(LoadWAHA);
|
||||
await this.pupPage.evaluate(LoadWAHACore);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,10 +169,10 @@ export class WebjsClientCore extends Client {
|
||||
}
|
||||
|
||||
const tags = ['receipt', 'presence', 'chatstate'];
|
||||
const WAWap = window.require('WAWap');
|
||||
// @ts-ignore
|
||||
window.decodeStanzaBack = window.Store.SocketWap.decodeStanza;
|
||||
// @ts-ignore
|
||||
window.Store.SocketWap.decodeStanza = async (...args) => {
|
||||
window.decodeStanzaBack = WAWap.decodeStanza;
|
||||
WAWap.decodeStanza = async (...args) => {
|
||||
// @ts-ignore
|
||||
const result = await window.decodeStanzaBack(...args);
|
||||
if (tags.includes(result?.tag)) {
|
||||
@@ -200,9 +193,9 @@ export class WebjsClientCore extends Client {
|
||||
async setPushName(name: string) {
|
||||
await this.ensureWahaInjected();
|
||||
await this.pupPage.evaluate(async (pushName) => {
|
||||
return await window['WAHA'].WAWebSetPushnameConnAction.setPushname(
|
||||
pushName,
|
||||
);
|
||||
return await window
|
||||
.require('WAWebSetPushnameConnAction')
|
||||
.setPushname(pushName);
|
||||
}, name);
|
||||
if (this.info) {
|
||||
this.info.pushname = name;
|
||||
@@ -211,16 +204,9 @@ export class WebjsClientCore extends Client {
|
||||
|
||||
async unpair() {
|
||||
await this.pupPage.evaluate(async () => {
|
||||
if (
|
||||
// @ts-ignore
|
||||
window.Store &&
|
||||
// @ts-ignore
|
||||
window.Store.AppState &&
|
||||
// @ts-ignore
|
||||
typeof window.Store.AppState.logout === 'function'
|
||||
) {
|
||||
// @ts-ignore
|
||||
await window.Store.AppState.logout();
|
||||
const Socket = window.require('WAWebSocketModel')?.Socket;
|
||||
if (Socket && typeof Socket.logout === 'function') {
|
||||
await Socket.logout();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -229,11 +215,9 @@ export class WebjsClientCore extends Client {
|
||||
await this.ensureWahaInjected();
|
||||
const labelId: number = (await this.pupPage.evaluate(
|
||||
async (name, color) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.WAWebBizLabelEditingAction.labelAddAction(
|
||||
name,
|
||||
color,
|
||||
);
|
||||
return await window
|
||||
.require('WAWebBizLabelEditingAction')
|
||||
.labelAddAction(name, color);
|
||||
},
|
||||
name,
|
||||
color,
|
||||
@@ -244,20 +228,16 @@ export class WebjsClientCore extends Client {
|
||||
async deleteLabel(label: Label) {
|
||||
await this.ensureWahaInjected();
|
||||
return await this.pupPage.evaluate(async (label) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.WAWebBizLabelEditingAction.labelDeleteAction(
|
||||
label.id,
|
||||
label.name,
|
||||
label.color,
|
||||
);
|
||||
return await window
|
||||
.require('WAWebBizLabelEditingAction')
|
||||
.labelDeleteAction(label.id, label.name, label.color);
|
||||
}, label);
|
||||
}
|
||||
|
||||
async updateLabel(label: Label) {
|
||||
await this.ensureWahaInjected();
|
||||
return await this.pupPage.evaluate(async (label) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.WAWebBizLabelEditingAction.labelEditAction(
|
||||
return await window.require('WAWebBizLabelEditingAction').labelEditAction(
|
||||
label.id,
|
||||
label.name,
|
||||
undefined, // predefinedId
|
||||
@@ -279,8 +259,27 @@ export class WebjsClientCore extends Client {
|
||||
|
||||
const chats = await this.pupPage.evaluate(
|
||||
async (pagination, filter) => {
|
||||
let chats = window
|
||||
.require('WAWebCollections')
|
||||
.Chat.getModelsArray()
|
||||
.slice();
|
||||
|
||||
// Filter chats by IDs if filter is provided
|
||||
if (filter && filter.ids && filter.ids.length > 0) {
|
||||
chats = chats.filter((chat) =>
|
||||
// @ts-ignore
|
||||
filter.ids.includes(window.WWebJS.GetSerialized(chat.id)),
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return await window.WAHA.getChats(pagination, filter);
|
||||
const paginator = new window.Paginator(pagination);
|
||||
chats = paginator.apply(chats);
|
||||
const chatPromises = chats.map((chat) =>
|
||||
// @ts-ignore
|
||||
window.WWebJS.getChatModel(chat),
|
||||
);
|
||||
return await Promise.all(chatPromises);
|
||||
},
|
||||
pagination,
|
||||
filter,
|
||||
@@ -292,7 +291,7 @@ export class WebjsClientCore extends Client {
|
||||
protected async ensureWahaInjected() {
|
||||
const hasWaha = await this.pupPage.evaluate(() => {
|
||||
// @ts-ignore
|
||||
return Boolean(window.WAHA && window.WAHA.getChats);
|
||||
return Boolean(window.Paginator);
|
||||
});
|
||||
if (!hasWaha) {
|
||||
await this.injectWaha();
|
||||
@@ -310,14 +309,14 @@ export class WebjsClientCore extends Client {
|
||||
font: status.font,
|
||||
};
|
||||
const sentMsg = await this.pupPage.evaluate(async (status) => {
|
||||
// @ts-ignore
|
||||
await window.Store.SendStatus.sendStatusTextMsgAction(status);
|
||||
// @ts-ignore
|
||||
const meUser = window.Store.User.getMaybeMePnUser();
|
||||
// @ts-ignore
|
||||
const myStatus = window.Store.Status.getModelsArray().findLast(
|
||||
(x) => x.id == meUser,
|
||||
);
|
||||
await window
|
||||
.require('WAWebSendStatusMsgAction')
|
||||
.sendStatusTextMsgAction(status);
|
||||
const meUser = window.require('WAWebUserPrefsMeUser').getMaybeMePnUser();
|
||||
const myStatus = window
|
||||
.require('WAWebCollections')
|
||||
.Status.getModelsArray()
|
||||
.findLast((x) => x.id == meUser);
|
||||
if (!myStatus) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -385,17 +384,20 @@ export class WebjsClientCore extends Client {
|
||||
// Construct the initial anchor the same way wa-js does:
|
||||
// serialize to string then reconstruct via MsgKey.fromString so the
|
||||
// object has the exact shape msgFindByDirection expects.
|
||||
const lastReceivedSerialized = chat.lastReceivedKey?._serialized;
|
||||
if (!lastReceivedSerialized) return [];
|
||||
// @ts-ignore
|
||||
let currentAnchorKey = window.Store.MsgKey.fromString(
|
||||
lastReceivedSerialized,
|
||||
const lastReceivedSerialized = window.WWebJS.GetSerialized(
|
||||
chat.lastReceivedKey,
|
||||
);
|
||||
if (!lastReceivedSerialized) return [];
|
||||
let currentAnchorKey = window
|
||||
.require('WAWebMsgKey')
|
||||
.fromString(lastReceivedSerialized);
|
||||
|
||||
// msgFindByDirection is exclusive of the anchor; include the anchor
|
||||
// message itself (the most recent message in the chat) upfront
|
||||
// @ts-ignore
|
||||
const anchorMsg = window.Store.Msg.get(lastReceivedSerialized);
|
||||
const anchorMsg = window
|
||||
.require('WAWebCollections')
|
||||
.Msg.get(lastReceivedSerialized);
|
||||
if (anchorMsg) {
|
||||
msgs.push(anchorMsg);
|
||||
}
|
||||
@@ -409,14 +411,14 @@ export class WebjsClientCore extends Client {
|
||||
// @ts-ignore
|
||||
const toModel = (m) => {
|
||||
if (m && typeof m.serialize === 'function') return m;
|
||||
const serializedId = m?.id?._serialized;
|
||||
// @ts-ignore
|
||||
const serializedId = window.WWebJS.GetSerialized(m?.id);
|
||||
const Msg = window.require('WAWebCollections').Msg;
|
||||
if (serializedId) {
|
||||
// @ts-ignore
|
||||
const stored = window.Store.Msg.get(serializedId);
|
||||
const stored = Msg.get(serializedId);
|
||||
if (stored) return stored;
|
||||
}
|
||||
// @ts-ignore
|
||||
return new window.Store.Msg.modelClass(m);
|
||||
return new Msg.modelClass(m);
|
||||
};
|
||||
|
||||
while (true) {
|
||||
@@ -441,7 +443,8 @@ export class WebjsClientCore extends Client {
|
||||
// appear in multiple batches when anchors overlap
|
||||
const seenIds = new Set();
|
||||
msgs = msgs.filter((m) => {
|
||||
const sid = m?.id?._serialized;
|
||||
// @ts-ignore
|
||||
const sid = window.WWebJS.GetSerialized(m?.id);
|
||||
if (!sid || seenIds.has(sid)) return false;
|
||||
seenIds.add(sid);
|
||||
return true;
|
||||
@@ -467,21 +470,22 @@ export class WebjsClientCore extends Client {
|
||||
// the oldest message in this batch — use it as the next anchor to
|
||||
// walk further back in history without overlap
|
||||
const oldestInBatch = batchModels[batchModels.length - 1];
|
||||
const oldestSerialized = oldestInBatch?.id?._serialized;
|
||||
if (!oldestSerialized) break;
|
||||
// @ts-ignore
|
||||
currentAnchorKey = window.Store.MsgKey.fromString(oldestSerialized);
|
||||
const oldestSerialized = window.WWebJS.GetSerialized(
|
||||
oldestInBatch?.id,
|
||||
);
|
||||
if (!oldestSerialized) break;
|
||||
currentAnchorKey = window
|
||||
.require('WAWebMsgKey')
|
||||
.fromString(oldestSerialized);
|
||||
}
|
||||
} else {
|
||||
// Legacy fallback: loadEarlierMsgs loop
|
||||
msgs = chat.msgs.getModelsArray();
|
||||
while (msgs.length < pagination.limit + pagination.offset) {
|
||||
const loadedMessages =
|
||||
// @ts-ignore
|
||||
await window.Store.ConversationMsgs.loadEarlierMsgs(
|
||||
chat,
|
||||
chat.msgs,
|
||||
);
|
||||
const loadedMessages = await window
|
||||
.require('WAWebChatLoadMessages')
|
||||
.loadEarlierMsgs(chat, chat.msgs);
|
||||
if (!loadedMessages || loadedMessages.length == 0) break;
|
||||
|
||||
msgs = [...loadedMessages, ...msgs];
|
||||
@@ -530,8 +534,7 @@ export class WebjsClientCore extends Client {
|
||||
pagination.offset ||= 0;
|
||||
pagination.sortBy ||= 'lid';
|
||||
|
||||
// @ts-ignore
|
||||
const WAWebApiContact = window.Store.LidUtils;
|
||||
const WAWebApiContact = window.require('WAWebApiContact');
|
||||
|
||||
await WAWebApiContact.warmUpAllLidPnMappings();
|
||||
const lidMap = WAWebApiContact.lidPnCache['$1'];
|
||||
@@ -539,9 +542,9 @@ export class WebjsClientCore extends Client {
|
||||
const result = values.map((map) => {
|
||||
return {
|
||||
// @ts-ignore
|
||||
lid: map.lid._serialized,
|
||||
lid: window.WWebJS.GetSerialized(map.lid),
|
||||
// @ts-ignore
|
||||
pn: map.phoneNumber._serialized,
|
||||
pn: window.WWebJS.GetSerialized(map.phoneNumber),
|
||||
};
|
||||
});
|
||||
// @ts-ignore
|
||||
@@ -556,8 +559,7 @@ export class WebjsClientCore extends Client {
|
||||
|
||||
public async getLidsCount(): Promise<number> {
|
||||
const count: number = (await this.pupPage.evaluate(async () => {
|
||||
// @ts-ignore
|
||||
const WAWebApiContact = window.Store.LidUtils;
|
||||
const WAWebApiContact = window.require('WAWebApiContact');
|
||||
|
||||
await WAWebApiContact.warmUpAllLidPnMappings();
|
||||
const lidMap = WAWebApiContact.lidPnCache['$1'];
|
||||
@@ -568,28 +570,26 @@ export class WebjsClientCore extends Client {
|
||||
|
||||
public async findPNByLid(lid: string): Promise<string> {
|
||||
const pn = await this.pupPage.evaluate(async (lid) => {
|
||||
// @ts-ignore
|
||||
const WAWebApiContact = window.Store.LidUtils;
|
||||
// @ts-ignore
|
||||
const WAWebWidFactory = window.Store.WidFactory;
|
||||
const WAWebApiContact = window.require('WAWebApiContact');
|
||||
const WAWebWidFactory = window.require('WAWebWidFactory');
|
||||
|
||||
const wid = WAWebWidFactory.createWid(lid);
|
||||
const result = WAWebApiContact.getPhoneNumber(wid);
|
||||
return result ? result._serialized : null;
|
||||
// @ts-ignore
|
||||
return window.WWebJS.GetSerialized(result);
|
||||
}, lid);
|
||||
return pn;
|
||||
}
|
||||
|
||||
public async findLIDByPhoneNumber(phoneNumber: string): Promise<string> {
|
||||
const lid: string = (await this.pupPage.evaluate(async (pn) => {
|
||||
// @ts-ignore
|
||||
const WAWebApiContact = window.Store.LidUtils;
|
||||
// @ts-ignore
|
||||
const WAWebWidFactory = window.Store.WidFactory;
|
||||
const WAWebApiContact = window.require('WAWebApiContact');
|
||||
const WAWebWidFactory = window.require('WAWebWidFactory');
|
||||
|
||||
const wid = WAWebWidFactory.createWid(pn);
|
||||
const result = WAWebApiContact.getCurrentLid(wid);
|
||||
return result ? result._serialized : null;
|
||||
// @ts-ignore
|
||||
return window.WWebJS.GetSerialized(result);
|
||||
}, phoneNumber)) as any;
|
||||
return lid;
|
||||
}
|
||||
@@ -629,7 +629,8 @@ export class WebjsClientCore extends Client {
|
||||
}
|
||||
return chatstates.map((chatstate) => {
|
||||
return {
|
||||
participant: chatstate.id._serialized,
|
||||
// @ts-ignore
|
||||
participant: window.WWebJS.GetSerialized(chatstate.id),
|
||||
lastSeen: chatstate.t,
|
||||
state: chatstate.type,
|
||||
};
|
||||
@@ -652,16 +653,18 @@ export class WebjsClientCore extends Client {
|
||||
inviteCode: string,
|
||||
limit: number,
|
||||
): Promise<WebjsChannelMessage[]> {
|
||||
await this.ensureWahaExtraInjected();
|
||||
const response: _GetNewsletterPreviewDataResponse =
|
||||
await this.pupPage.evaluate(
|
||||
async (code, limit) => {
|
||||
// Overwrite limit
|
||||
window['WAHA'].WAWebNewsletterGatingUtils = () => limit;
|
||||
// Overwrite the server-side message count so the preview fetches
|
||||
// exactly `limit` messages
|
||||
window.require(
|
||||
'WAWebNewsletterGatingUtils',
|
||||
).getMaxMsgCountFromServer = () => limit;
|
||||
|
||||
const result = await window[
|
||||
'WAHA'
|
||||
].WAWebNewsletterPreviewJob.getNewsletterPreviewData(code, 'guest');
|
||||
const result = await window
|
||||
.require('WAWebNewsletterPreviewJob')
|
||||
.getNewsletterPreviewData(code, 'guest');
|
||||
for (const newsletterReaction of result.newsletterReactions) {
|
||||
// puppeter doesn't support Map,
|
||||
// so we need to convert it to object
|
||||
@@ -671,11 +674,9 @@ export class WebjsClientCore extends Client {
|
||||
}
|
||||
|
||||
// Fetch one more time to save in database so we can fetch media later
|
||||
await window[
|
||||
'WAHA'
|
||||
].WAWebLoadNewsletterPreviewChatAction.loadNewsletterPreviewChat(
|
||||
code,
|
||||
);
|
||||
await window
|
||||
.require('WAWebLoadNewsletterPreviewChatAction')
|
||||
.loadNewsletterPreviewChat(code);
|
||||
return result;
|
||||
},
|
||||
inviteCode,
|
||||
@@ -693,7 +694,7 @@ export class WebjsClientCore extends Client {
|
||||
const messages: WebjsChannelMessage[] = messageInstances.map((msg) => {
|
||||
return {
|
||||
message: msg,
|
||||
reactions: reactions.get(msg.id._serialized) || [],
|
||||
reactions: reactions.get(GetSerialized(msg.id)) || [],
|
||||
viewCount: msg.rawData.viewCount,
|
||||
};
|
||||
});
|
||||
@@ -704,40 +705,20 @@ export class WebjsClientCore extends Client {
|
||||
* Channels Search methods
|
||||
*/
|
||||
async searchChannelsView(params: any): Promise<any> {
|
||||
await this.ensureWahaExtraInjected();
|
||||
const newsletters: any = await this.pupPage.evaluate(async (params) => {
|
||||
return await window[
|
||||
'WAHA'
|
||||
].WAWebNewsletterDirectorySearchJob.getNewsletterDirectoryList(params);
|
||||
return await window
|
||||
.require('WAWebNewsletterDirectorySearchJob')
|
||||
.getNewsletterDirectoryList(params);
|
||||
}, params);
|
||||
return newsletters;
|
||||
}
|
||||
|
||||
async searchChannelsText(params: any): Promise<any> {
|
||||
await this.ensureWahaExtraInjected();
|
||||
const newsletters: any = await this.pupPage.evaluate(async (params) => {
|
||||
return await window[
|
||||
'WAHA'
|
||||
].WAWebNewsletterDirectorySearchJob.getNewsletterDirectorySearchResults(
|
||||
params,
|
||||
);
|
||||
return await window
|
||||
.require('WAWebNewsletterDirectorySearchJob')
|
||||
.getNewsletterDirectorySearchResults(params);
|
||||
}, params);
|
||||
return newsletters;
|
||||
}
|
||||
|
||||
protected async ensureWahaExtraInjected() {
|
||||
const hasWahaExtra = await this.pupPage.evaluate(() => {
|
||||
return Boolean(
|
||||
// @ts-ignore
|
||||
window.WAHA &&
|
||||
// @ts-ignore
|
||||
window.WAHA.WAWebNewsletterDirectorySearchJob &&
|
||||
// @ts-ignore
|
||||
window.WAHA.WAWebNewsletterPreviewJob,
|
||||
);
|
||||
});
|
||||
if (!hasWahaExtra) {
|
||||
await this.injectWaha();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
exports.LoadWAHA = () => {
|
||||
window.WAHA = {};
|
||||
window.WAHA.WAWebBizLabelEditingAction = window.require(
|
||||
'WAWebBizLabelEditingAction',
|
||||
);
|
||||
|
||||
window.WAHA.getChats = async (pagination, filter) => {
|
||||
let chats = window.Store.Chat.getModelsArray().slice();
|
||||
|
||||
// Filter chats by IDs if filter is provided
|
||||
if (filter && filter.ids && filter.ids.length > 0) {
|
||||
chats = chats.filter((chat) => filter.ids.includes(chat.id._serialized));
|
||||
}
|
||||
|
||||
const paginator = new window.Paginator(pagination);
|
||||
chats = paginator.apply(chats);
|
||||
const chatPromises = chats.map((chat) => window.WWebJS.getChatModel(chat));
|
||||
return await Promise.all(chatPromises);
|
||||
};
|
||||
|
||||
// Set push name
|
||||
window.WAHA.WAWebSetPushnameConnAction = window.require(
|
||||
'WAWebSetPushnameConnAction',
|
||||
);
|
||||
|
||||
// Get my status
|
||||
window.WAHA.WAWebTextStatusCollection = window.require(
|
||||
'WAWebTextStatusCollection',
|
||||
);
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
exports.LoadWAHACore = () => {
|
||||
// Channels Search
|
||||
window.WAHA.WAWebNewsletterDirectorySearchJob = window.require(
|
||||
'WAWebNewsletterDirectorySearchJob',
|
||||
);
|
||||
// Channels load "preview" - fetch messages by invite code
|
||||
window.WAHA.WAWebNewsletterPreviewJob = window.require(
|
||||
'WAWebNewsletterPreviewJob',
|
||||
);
|
||||
window.WAHA.WAWebLoadNewsletterPreviewChatAction = window.require(
|
||||
'WAWebLoadNewsletterPreviewChatAction',
|
||||
);
|
||||
|
||||
// Rewrite newsletter count to different number
|
||||
window.WAHA.WAWebNewsletterGatingUtils = window.require(
|
||||
'WAWebNewsletterGatingUtils',
|
||||
);
|
||||
};
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
GroupParticipant as WEBJSGroupParticipant,
|
||||
} from 'whatsapp-web.js';
|
||||
import { isPnUser } from '@waha/core/utils/jids';
|
||||
import { GetSerialized } from '@waha/core/utils/serialized';
|
||||
|
||||
function ToGroupInfo(
|
||||
group: GroupChat,
|
||||
@@ -29,11 +30,11 @@ function ToGroupInfo(
|
||||
const groupMetadata = group.groupMetadata;
|
||||
const info: GroupInfo = {
|
||||
// @ts-ignore
|
||||
id: group.id._serialized,
|
||||
id: GetSerialized(group.id),
|
||||
subject: group.name,
|
||||
description: group.description,
|
||||
invite: invite,
|
||||
membersCanAddNewMember: groupMetadata.restrict,
|
||||
membersCanAddNewMember: groupMetadata.memberAddMode === 'all_member_add',
|
||||
membersCanSendMessages: groupMetadata.announce,
|
||||
newMembersApprovalRequired: groupMetadata.membershipApprovalMode,
|
||||
participants: participants,
|
||||
@@ -72,11 +73,10 @@ export function getParticipants(
|
||||
role = GroupParticipantRole.ADMIN;
|
||||
}
|
||||
|
||||
const participantId = GetSerialized(participant.id);
|
||||
return {
|
||||
id: participant.id._serialized,
|
||||
pn: isPnUser(participant.id._serialized)
|
||||
? participant.id._serialized
|
||||
: null,
|
||||
id: participantId,
|
||||
pn: isPnUser(participantId) ? participantId : null,
|
||||
role: role,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -108,6 +108,7 @@ import {
|
||||
GroupParticipant,
|
||||
GroupSortField,
|
||||
ParticipantsRequest,
|
||||
SettingsMemberAddMode,
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '@waha/structures/groups.dto';
|
||||
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
|
||||
@@ -125,7 +126,11 @@ import {
|
||||
WAMessageReaction,
|
||||
} from '@waha/structures/responses.dto';
|
||||
import { BrowserTraceQuery } from '@waha/structures/server.debug.dto';
|
||||
import { MeInfo } from '@waha/structures/sessions.dto';
|
||||
import {
|
||||
MeInfo,
|
||||
ReachoutTimelockEnforcementType,
|
||||
} from '@waha/structures/sessions.dto';
|
||||
import { EnsureSeconds } from '@waha/utils/timehelper';
|
||||
import {
|
||||
BROADCAST_ID,
|
||||
DeleteStatusRequest,
|
||||
@@ -182,6 +187,7 @@ import {
|
||||
Message as MessageInstance,
|
||||
Call as CallInstance,
|
||||
} from 'whatsapp-web.js/src/structures';
|
||||
import { GetSerialized } from '@waha/core/utils/serialized';
|
||||
|
||||
import { WAJSPresenceChatStateType, WebJSPresence } from './types';
|
||||
import { WebJSAuthFactory } from './WebJSAuthFactory';
|
||||
@@ -531,8 +537,10 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
const wid = clientInfo.wid;
|
||||
return {
|
||||
id: wid?._serialized,
|
||||
id: GetSerialized(wid),
|
||||
lid: GetSerialized(clientInfo.lid),
|
||||
pushName: clientInfo?.pushname,
|
||||
reachoutTimelock: this.reachoutTimelock.value,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -549,6 +557,28 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
protected listenConnectionEvents() {
|
||||
this.whatsapp.on(Events.REACHOUT_TIMELOCK_UPDATE, (record: any) => {
|
||||
if (!record) {
|
||||
// The app removes the stored record when the enforcement is lifted (or there is none)
|
||||
const current = this.reachoutTimelock.value;
|
||||
if (current?.isActive) {
|
||||
this.reachoutTimelock.update({ ...current, isActive: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
const enforcementType =
|
||||
record.enforcement_type ?? ReachoutTimelockEnforcementType.DEFAULT;
|
||||
this.reachoutTimelock.update({
|
||||
enforcementType: enforcementType as ReachoutTimelockEnforcementType,
|
||||
// The record only exists while the enforcement is active
|
||||
isActive: true,
|
||||
// The app stores 'time_enforcement_ends' as unix milliseconds
|
||||
timeEnforcementEnds: record.time_enforcement_ends
|
||||
? EnsureSeconds(record.time_enforcement_ends)
|
||||
: null,
|
||||
});
|
||||
});
|
||||
|
||||
this.whatsapp.on(Events.QR_RECEIVED, async (qr) => {
|
||||
this.logger.debug('QR received');
|
||||
// Convert to image and save
|
||||
@@ -664,14 +694,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
|
||||
private async loadClientInfo() {
|
||||
const data = await this.whatsapp.pupPage.evaluate(() => {
|
||||
const WAWebUserPrefsMeUser = window.require('WAWebUserPrefsMeUser');
|
||||
return {
|
||||
// @ts-ignore
|
||||
...window.Store.Conn.serialize(),
|
||||
...window.require('WAWebConnModel').Conn.serialize(),
|
||||
wid:
|
||||
// @ts-ignore
|
||||
window.Store.User.getMaybeMePnUser() ||
|
||||
// @ts-ignore
|
||||
window.Store.User.getMaybeMeLidUser(),
|
||||
WAWebUserPrefsMeUser.getMaybeMePnUser() ||
|
||||
WAWebUserPrefsMeUser.getMaybeMeLidUser(),
|
||||
};
|
||||
});
|
||||
this.whatsapp.info = data as any;
|
||||
@@ -740,7 +768,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
return {
|
||||
numberExists: true,
|
||||
chatId: result._serialized,
|
||||
chatId: GetSerialized(result),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1137,14 +1165,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
|
||||
protected async fetchChatSummary(chat: Chat): Promise<ChatSummary> {
|
||||
const picture = await this.getContactProfilePicture(
|
||||
chat.id._serialized,
|
||||
GetSerialized(chat.id),
|
||||
false,
|
||||
);
|
||||
const lastMessage = chat.lastMessage
|
||||
? this.toWAMessage(chat.lastMessage)
|
||||
: null;
|
||||
return {
|
||||
id: chat.id._serialized,
|
||||
id: GetSerialized(chat.id),
|
||||
name: chat.name || null,
|
||||
picture: picture,
|
||||
lastMessage: lastMessage,
|
||||
@@ -1250,7 +1278,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
// @ts-ignore
|
||||
message.rawData.receipts = await message.getInfo().catch((error) => {
|
||||
this.logger.error(
|
||||
{ error: error, msg: message.id._serialized },
|
||||
{ error: error, msg: GetSerialized(message.id) },
|
||||
'Failed to get receipts',
|
||||
);
|
||||
return null;
|
||||
@@ -1508,6 +1536,22 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
return groupChat.setMessagesAdminsOnly(value);
|
||||
}
|
||||
|
||||
public async getMemberAddMode(id): Promise<SettingsMemberAddMode> {
|
||||
const groupChat = (await this.whatsapp.getChatById(id)) as GroupChat;
|
||||
return {
|
||||
membersCanAddNewMember:
|
||||
// @ts-ignore
|
||||
groupChat.groupMetadata.memberAddMode === 'all_member_add',
|
||||
};
|
||||
}
|
||||
|
||||
@Activity()
|
||||
public async setMemberAddMode(id, value) {
|
||||
const groupChat = (await this.whatsapp.getChatById(id)) as GroupChat;
|
||||
// The library setter is inverted - it takes "adminsOnly"
|
||||
return groupChat.setAddMembersAdminsOnly(!value);
|
||||
}
|
||||
|
||||
public async getGroups(pagination: PaginationParams) {
|
||||
const chats = await this.whatsapp.getChats();
|
||||
const groups = lodash.filter(chats, (chat) => chat.isGroup);
|
||||
@@ -1715,7 +1759,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
role = ChannelRole.GUEST;
|
||||
}
|
||||
return {
|
||||
id: chat.id._serialized,
|
||||
id: GetSerialized(chat.id),
|
||||
name: chat.name,
|
||||
description: chat.description,
|
||||
invite: getChannelInviteLink(metadata.inviteCode),
|
||||
@@ -2214,10 +2258,10 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
},
|
||||
);
|
||||
const chatsArchived$ = chatArchived$.pipe(
|
||||
filter((event: any) => this.jids.include(event?.chat?.id?._serialized)),
|
||||
filter((event: any) => this.jids.include(GetSerialized(event?.chat?.id))),
|
||||
map((event) => {
|
||||
return {
|
||||
id: event.chat.id._serialized,
|
||||
id: GetSerialized(event.chat.id),
|
||||
archived: event.archived,
|
||||
timestamp: event.chat.timestamp,
|
||||
};
|
||||
@@ -2304,7 +2348,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
|
||||
const source = this.getMessageSource(reaction.id.id);
|
||||
return {
|
||||
id: reaction.id._serialized,
|
||||
id: GetSerialized(reaction.id),
|
||||
from: normalizeJid(reaction.senderId),
|
||||
fromMe: reaction.id.fromMe,
|
||||
source: source,
|
||||
@@ -2313,13 +2357,13 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
timestamp: reaction.timestamp,
|
||||
reaction: {
|
||||
text: reaction.reaction,
|
||||
messageId: reaction.msgId._serialized,
|
||||
messageId: GetSerialized(reaction.msgId),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private toPollVotePayload(vote: WebjsPollVote): PollVotePayload | null {
|
||||
const pollMessageId = vote?.parentMessage?.id?._serialized;
|
||||
const pollMessageId = GetSerialized(vote?.parentMessage?.id);
|
||||
if (!pollMessageId) {
|
||||
return null;
|
||||
}
|
||||
@@ -2399,10 +2443,10 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
protected toWAMessage(message: Message): WAMessage {
|
||||
const replyTo = this.extractReplyTo(message);
|
||||
const source = this.getMessageSource(message.id.id);
|
||||
const key = parseMessageIdSerialized(message.id._serialized);
|
||||
const key = parseMessageIdSerialized(GetSerialized(message.id));
|
||||
// @ts-ignore
|
||||
return {
|
||||
id: message.id._serialized,
|
||||
id: GetSerialized(message.id),
|
||||
timestamp: message.timestamp,
|
||||
from: message.from,
|
||||
fromMe: message.fromMe,
|
||||
@@ -2476,7 +2520,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
|
||||
protected toWAContact(contact: Contact) {
|
||||
// @ts-ignore
|
||||
contact.id = contact.id._serialized;
|
||||
contact.id = GetSerialized(contact.id);
|
||||
return contact;
|
||||
}
|
||||
|
||||
@@ -2554,7 +2598,7 @@ export class WEBJSEngineMediaProcessor
|
||||
}
|
||||
|
||||
getMessageId(message: Message): string {
|
||||
return message.id._serialized;
|
||||
return GetSerialized(message.id);
|
||||
}
|
||||
|
||||
getMimetype(message: Message): string {
|
||||
|
||||
@@ -165,6 +165,7 @@ import {
|
||||
GroupParticipantRole,
|
||||
GroupSortField,
|
||||
ParticipantsRequest,
|
||||
SettingsMemberAddMode,
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '@waha/structures/groups.dto';
|
||||
import { ContactQuery, ContactRequest } from '@waha/structures/contacts.dto';
|
||||
@@ -1412,6 +1413,26 @@ export class WhatsappSessionWPPCore extends WhatsappSession {
|
||||
);
|
||||
}
|
||||
|
||||
public async getMemberAddMode(id): Promise<SettingsMemberAddMode> {
|
||||
const group = await this.wpp!.getChatById(this.ensureSuffix(id));
|
||||
// Undocumented property - 'all_member_add' or 'admin_add'
|
||||
const memberAddMode = group?.groupMetadata?.memberAddMode;
|
||||
return {
|
||||
membersCanAddNewMember: memberAddMode === 'all_member_add',
|
||||
};
|
||||
}
|
||||
|
||||
@Activity()
|
||||
public setMemberAddMode(id: string, value: boolean): Promise<boolean> {
|
||||
// Not in the GroupProperty enum, but wa-js passes the property through
|
||||
// to WhatsApp's setGroupProperty - 1 - all members, 0 - admins only
|
||||
return this.wpp!.setGroupProperty(
|
||||
this.ensureSuffix(id),
|
||||
'member_add_mode' as GroupProperty,
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
@Activity()
|
||||
public deleteGroup(id: string): Promise<boolean> {
|
||||
return this.wpp!.deleteChat(this.ensureSuffix(id));
|
||||
@@ -1874,7 +1895,8 @@ export class WhatsappSessionWPPCore extends WhatsappSession {
|
||||
subject: group?.name || '',
|
||||
description: group?.groupMetadata?.desc || '',
|
||||
invite: null,
|
||||
membersCanAddNewMember: !group?.groupMetadata?.restrict,
|
||||
membersCanAddNewMember:
|
||||
group?.groupMetadata?.memberAddMode === 'all_member_add',
|
||||
membersCanSendMessages: !group?.groupMetadata?.announce,
|
||||
newMembersApprovalRequired:
|
||||
!!group?.groupMetadata?.membershipApprovalMode,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@waha/apps/app_sdk/services/IAppsService';
|
||||
import { EngineBootstrap } from '@waha/core/abc/EngineBootstrap';
|
||||
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
|
||||
import { NowebEngineConfigService } from '@waha/core/config/NowebEngineConfigService';
|
||||
import { WPPEngineConfigService } from '@waha/core/config/WPPEngineConfigService';
|
||||
import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService';
|
||||
import { WhatsappSessionGoWSCore } from '@waha/core/engines/gows/session.gows.core';
|
||||
@@ -96,6 +97,7 @@ export class SessionManagerCore
|
||||
private engineConfigService: EngineConfigService,
|
||||
private webjsEngineConfigService: WebJSEngineConfigService,
|
||||
private wppEngineConfigService: WPPEngineConfigService,
|
||||
private nowebEngineConfigService: NowebEngineConfigService,
|
||||
gowsConfigService: GowsEngineConfigService,
|
||||
log: PinoLogger,
|
||||
private mediaStorageFactory: MediaStorageFactory,
|
||||
@@ -235,14 +237,20 @@ export class SessionManagerCore
|
||||
this.log.info(`Restarting sessions with delay of ${sleepS} seconds...`);
|
||||
const sleepMs = this.config.autoStartDelaySeconds * 1000;
|
||||
for (const sessionName of sessions) {
|
||||
const log = this.log.logger.child({ session: sessionName });
|
||||
await this.withLock(sessionName, async () => {
|
||||
const log = this.log.logger.child({ session: sessionName });
|
||||
log.info(`Restarting STOPPED session...`);
|
||||
await this.start(sessionName).catch((error) => {
|
||||
log.error(`Failed to start STOPPED session: ${error}`);
|
||||
log.error(error.stack);
|
||||
});
|
||||
});
|
||||
})
|
||||
// withLock() itself can reject (e.g. "Maximum execution time is exceeded"),
|
||||
// catch it so one stuck session doesn't abort restarting the rest
|
||||
.catch((error) => {
|
||||
log.error(`Failed to restart STOPPED session: ${error}`);
|
||||
log.error(error.stack);
|
||||
});
|
||||
await sleep(sleepMs);
|
||||
}
|
||||
this.log.info(`STOPPED sessions have been restarted.`);
|
||||
@@ -358,6 +366,8 @@ export class SessionManagerCore
|
||||
sessionConfig.engineConfig = this.wppEngineConfigService.getConfig();
|
||||
} else if (this.EngineClass === WhatsappSessionGoWSCore) {
|
||||
sessionConfig.engineConfig = this.gowsConfigService.getConfig();
|
||||
} else if (this.EngineClass === WhatsappSessionNoWebCore) {
|
||||
sessionConfig.engineConfig = this.nowebEngineConfigService.getConfig();
|
||||
}
|
||||
// @ts-ignore
|
||||
const session = new this.EngineClass(sessionConfig);
|
||||
|
||||
@@ -187,7 +187,14 @@ export class MediaManager implements IMediaManager {
|
||||
const retryOptions = this.RETRY_OPTIONS;
|
||||
try {
|
||||
return await promiseRetry((retry: CallableFunction, number: number) => {
|
||||
return fn().catch(retry);
|
||||
return fn().catch((err: any) => {
|
||||
// Some failures are definitive (e.g. media not downloadable): retrying
|
||||
// won't help and may block, so abort the retry loop immediately.
|
||||
if (err?.nonRetriable) {
|
||||
throw err;
|
||||
}
|
||||
return retry(err);
|
||||
});
|
||||
}, retryOptions);
|
||||
} catch (error) {
|
||||
this.log.error(
|
||||
|
||||
@@ -108,7 +108,7 @@ export class MediaLocalStorage implements IMediaStorage {
|
||||
this.log.debug(`File ${filepath} was removed`);
|
||||
}),
|
||||
delay,
|
||||
);
|
||||
).unref();
|
||||
}
|
||||
|
||||
async close() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { SessionActions } from '@waha/core/auth/casl.types';
|
||||
import {
|
||||
ApiKey,
|
||||
IApiKeyRepository,
|
||||
@@ -10,6 +11,24 @@ import {
|
||||
import { ApiKeyDTO, ApiKeyRequest } from '@waha/structures/apikeys.dto';
|
||||
import { generatePrefixedId, generateSecret } from '@waha/utils/ids';
|
||||
|
||||
const MEDIA_KEY_ACTIONS: SessionActions = {
|
||||
read: false,
|
||||
send: false,
|
||||
control: false,
|
||||
setting: false,
|
||||
app: false,
|
||||
delete: false,
|
||||
};
|
||||
|
||||
const CONTROL_KEY_ACTIONS: SessionActions = {
|
||||
read: false,
|
||||
send: false,
|
||||
control: true,
|
||||
setting: false,
|
||||
app: false,
|
||||
delete: false,
|
||||
};
|
||||
|
||||
function CheckInvariant(
|
||||
apiKey: Pick<ApiKey, 'isAdmin' | 'session' | 'actions'>,
|
||||
): void {
|
||||
@@ -78,6 +97,46 @@ export class ApiKeyService {
|
||||
return this.toDTO(apikey);
|
||||
}
|
||||
|
||||
private async createOrGetScopedKey(
|
||||
session: string,
|
||||
variant: string,
|
||||
actions: SessionActions,
|
||||
): Promise<ApiKeyDTO> {
|
||||
const exists = await this.manager.exists(session);
|
||||
if (!exists) {
|
||||
throw new UnprocessableEntityException(
|
||||
`Session "${session}" does not exist`,
|
||||
);
|
||||
}
|
||||
// Deterministic id keeps this idempotent: repeated calls reuse the same
|
||||
// stored secret instead of minting a new key every time.
|
||||
const id = `key_id_${variant}_${session}`;
|
||||
const existing = await this.repository.getById(id);
|
||||
if (existing) {
|
||||
return this.toDTO(existing);
|
||||
}
|
||||
const apikey: ApiKey = {
|
||||
id: id,
|
||||
key: `key_${generateSecret(32)}`,
|
||||
isActive: true,
|
||||
isAdmin: false,
|
||||
session: session,
|
||||
actions: actions,
|
||||
app_id: null,
|
||||
};
|
||||
CheckInvariant(apikey);
|
||||
await this.repository.upsert(apikey);
|
||||
return this.toDTO(apikey);
|
||||
}
|
||||
|
||||
async createOrGetMediaKey(session: string): Promise<ApiKeyDTO> {
|
||||
return this.createOrGetScopedKey(session, 'media', MEDIA_KEY_ACTIONS);
|
||||
}
|
||||
|
||||
async createOrGetControlKey(session: string): Promise<ApiKeyDTO> {
|
||||
return this.createOrGetScopedKey(session, 'control', CONTROL_KEY_ACTIONS);
|
||||
}
|
||||
|
||||
async list(): Promise<ApiKeyDTO[]> {
|
||||
const keys = await this.repository.list();
|
||||
return keys.map((key) => this.toDTO(key));
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
GetSerialized,
|
||||
GetSerializedMsgKey,
|
||||
GetSerializedWid,
|
||||
} from './serialized';
|
||||
|
||||
describe('serialized helpers', () => {
|
||||
describe('GetSerializedWid', () => {
|
||||
it('returns strings and nullish values as-is', () => {
|
||||
expect(GetSerializedWid('123@c.us')).toBe('123@c.us');
|
||||
expect(GetSerializedWid(null)).toBeNull();
|
||||
expect(GetSerializedWid(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('uses existing _serialized', () => {
|
||||
expect(GetSerializedWid({ _serialized: '123@c.us' })).toBe('123@c.us');
|
||||
});
|
||||
|
||||
it('ignores the minified $1 property and reconstructs from keys', () => {
|
||||
const wid: any = { $1: 'ignored', user: '123', server: 'c.us' };
|
||||
expect(GetSerializedWid(wid)).toBe('123@c.us');
|
||||
expect(wid._serialized).toBe('123@c.us');
|
||||
});
|
||||
|
||||
it('reconstructs from user/server and caches', () => {
|
||||
const wid: any = { user: '123', server: 'c.us' };
|
||||
expect(GetSerializedWid(wid)).toBe('123@c.us');
|
||||
expect(wid._serialized).toBe('123@c.us');
|
||||
});
|
||||
|
||||
it('reconstructs a group wid', () => {
|
||||
expect(GetSerializedWid({ user: '123-456', server: 'g.us' })).toBe(
|
||||
'123-456@g.us',
|
||||
);
|
||||
});
|
||||
|
||||
it('appends the device when present and truthy', () => {
|
||||
expect(GetSerializedWid({ user: '123', server: 'c.us', device: 7 })).toBe(
|
||||
'123:7@c.us',
|
||||
);
|
||||
expect(GetSerializedWid({ user: '123', server: 'c.us', device: 0 })).toBe(
|
||||
'123@c.us',
|
||||
);
|
||||
});
|
||||
|
||||
it('handles the special "call" wid', () => {
|
||||
expect(GetSerializedWid({ user: 'call', server: 'call' })).toBe('call');
|
||||
});
|
||||
|
||||
it('returns null when it cannot reconstruct (no keys, only $1)', () => {
|
||||
expect(GetSerializedWid({ foo: 'bar' })).toBeNull();
|
||||
expect(GetSerializedWid({ $1: '123@c.us' })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetSerializedMsgKey', () => {
|
||||
it('reconstructs fromMe_remote_id and caches', () => {
|
||||
const key: any = {
|
||||
fromMe: true,
|
||||
remote: { _serialized: '123@c.us' },
|
||||
id: 'AAA',
|
||||
};
|
||||
expect(GetSerializedMsgKey(key)).toBe('true_123@c.us_AAA');
|
||||
expect(key._serialized).toBe('true_123@c.us_AAA');
|
||||
});
|
||||
|
||||
it('reconstructs a remote wid that only has component keys', () => {
|
||||
const key: any = {
|
||||
fromMe: false,
|
||||
remote: { user: '123', server: 'c.us' },
|
||||
id: 'AAA',
|
||||
};
|
||||
expect(GetSerializedMsgKey(key)).toBe('false_123@c.us_AAA');
|
||||
});
|
||||
|
||||
it('appends the participant', () => {
|
||||
const key: any = {
|
||||
fromMe: false,
|
||||
remote: { _serialized: '123-456@g.us' },
|
||||
id: 'AAA',
|
||||
participant: { _serialized: '789@c.us' },
|
||||
};
|
||||
expect(GetSerializedMsgKey(key)).toBe('false_123-456@g.us_AAA_789@c.us');
|
||||
});
|
||||
|
||||
it('appends self before participant', () => {
|
||||
const key: any = {
|
||||
fromMe: true,
|
||||
remote: { _serialized: '123@c.us' },
|
||||
id: 'AAA',
|
||||
self: 'out',
|
||||
participant: { _serialized: '789@c.us' },
|
||||
};
|
||||
expect(GetSerializedMsgKey(key)).toBe('true_123@c.us_AAA_out_789@c.us');
|
||||
});
|
||||
|
||||
it('ignores $1 and reconstructs from keys', () => {
|
||||
const key: any = {
|
||||
$1: 'ignored',
|
||||
fromMe: true,
|
||||
remote: { _serialized: '999@c.us' },
|
||||
id: 'BBB',
|
||||
};
|
||||
expect(GetSerializedMsgKey(key)).toBe('true_999@c.us_BBB');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetSerialized (dispatcher)', () => {
|
||||
it('dispatches wid shapes', () => {
|
||||
expect(GetSerialized({ user: '123', server: 'c.us' })).toBe('123@c.us');
|
||||
});
|
||||
|
||||
it('dispatches msgkey shapes', () => {
|
||||
expect(
|
||||
GetSerialized({
|
||||
fromMe: true,
|
||||
remote: { user: '123', server: 'c.us' },
|
||||
id: 'AAA',
|
||||
}),
|
||||
).toBe('true_123@c.us_AAA');
|
||||
});
|
||||
|
||||
it('honours existing _serialized before dispatching', () => {
|
||||
expect(GetSerialized({ _serialized: 'x@c.us' })).toBe('x@c.us');
|
||||
});
|
||||
|
||||
it('returns null for unknown shapes', () => {
|
||||
expect(GetSerialized({ foo: 'bar' })).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Serialized-ID helpers for the WEBJS engine.
|
||||
*
|
||||
* COPIED from whatsapp-web.js `src/util/Serialized.js` (fork: ../whatsapp-web.js).
|
||||
* Keep this file in sync with that upstream version whenever it changes.
|
||||
*
|
||||
* WhatsApp Web renamed the `_serialized` property on its ID objects (Wid /
|
||||
* MsgKey) to a minified name (`$1`) in the 2026-07 update. Rather than depend on
|
||||
* that unstable minified name, each helper reconstructs the serialized string
|
||||
* deterministically from the object's own component keys - existing
|
||||
* `_serialized` -> reconstruct from keys - and caches it back onto the object as
|
||||
* `_serialized` so later reads keep working.
|
||||
*/
|
||||
|
||||
interface CacheHit {
|
||||
done: boolean;
|
||||
value: string | null;
|
||||
}
|
||||
|
||||
function cached(id: any): CacheHit {
|
||||
if (id == null) {
|
||||
return { done: true, value: id };
|
||||
}
|
||||
if (typeof id === 'string') {
|
||||
return { done: true, value: id };
|
||||
}
|
||||
if (typeof id._serialized === 'string' && id._serialized !== '') {
|
||||
return { done: true, value: id._serialized };
|
||||
}
|
||||
return { done: false, value: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a Wid (WhatsApp ID): `user[:device]@server`.
|
||||
*/
|
||||
export function GetSerializedWid(id: any): string | null {
|
||||
const hit = cached(id);
|
||||
if (hit.done) {
|
||||
return hit.value;
|
||||
}
|
||||
|
||||
let value: string | null;
|
||||
if (id.user != null && id.server != null) {
|
||||
value =
|
||||
id.user === 'call'
|
||||
? 'call'
|
||||
: `${id.user}${id.device ? `:${id.device}` : ''}@${id.server}`;
|
||||
} else {
|
||||
value = null;
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
id._serialized = value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a MsgKey (message ID): `fromMe_remote_id[_self][_participant]`.
|
||||
*/
|
||||
export function GetSerializedMsgKey(id: any): string | null {
|
||||
const hit = cached(id);
|
||||
if (hit.done) {
|
||||
return hit.value;
|
||||
}
|
||||
|
||||
let value: string | null;
|
||||
if (id.remote != null && id.id != null) {
|
||||
const remote = GetSerializedWid(id.remote);
|
||||
const participant =
|
||||
id.participant != null ? GetSerializedWid(id.participant) : null;
|
||||
value =
|
||||
`${id.fromMe ? 'true' : 'false'}_${remote}_${id.id}` +
|
||||
(id.self ? `_${id.self}` : '') +
|
||||
(participant ? `_${participant}` : '');
|
||||
} else {
|
||||
value = null;
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
id._serialized = value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic entry point: resolves `_serialized`/`$1` then dispatches to the Wid
|
||||
* or MsgKey helper based on the object's shape. Use the specific helpers when
|
||||
* the id type is known.
|
||||
*/
|
||||
export function GetSerialized(id: any): string | null {
|
||||
const hit = cached(id);
|
||||
if (hit.done) {
|
||||
return hit.value;
|
||||
}
|
||||
if (id.remote != null && id.id != null) {
|
||||
return GetSerializedMsgKey(id);
|
||||
}
|
||||
if (id.user != null && id.server != null) {
|
||||
return GetSerializedWid(id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { applyDecorators } from '@nestjs/common';
|
||||
import { IsString, Matches, MaxLength } from 'class-validator';
|
||||
|
||||
// Affect almost all Databases - Sqlite, MongoDB, Postgres.
|
||||
const DB_NAME_LIMIT = 64;
|
||||
const DB_NAME_MAX_PREFIX_LEN = 'waha_noweb'.length;
|
||||
|
||||
export function SessionName() {
|
||||
return applyDecorators(
|
||||
IsString(),
|
||||
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',
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { SessionActions } from '@waha/core/auth/casl.types';
|
||||
import { SessionName } from '@waha/structures/sessions.dto';
|
||||
import { SessionName } from '@waha/nestjs/validation/SessionName';
|
||||
|
||||
export class SessionActionsDTO implements SessionActions {
|
||||
@ApiProperty({
|
||||
@@ -76,6 +76,12 @@ export class ApiKeyDTO {
|
||||
actions: SessionActions | null;
|
||||
}
|
||||
|
||||
export class ScopedApiKeyRequest {
|
||||
@ApiProperty({ example: 'default' })
|
||||
@SessionName()
|
||||
session: string;
|
||||
}
|
||||
|
||||
export class ApiKeyRequest {
|
||||
@ApiProperty({ example: false })
|
||||
@IsBoolean()
|
||||
|
||||
@@ -38,3 +38,115 @@ export class RequestCodeRequest {
|
||||
export class PairingCodeResponse {
|
||||
code: string;
|
||||
}
|
||||
|
||||
export class PasskeyAssertionResponseData {
|
||||
@ApiProperty({
|
||||
description: 'Base64url-encoded clientDataJSON from the authenticator.',
|
||||
})
|
||||
clientDataJSON: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Base64url-encoded authenticatorData from the authenticator.',
|
||||
})
|
||||
authenticatorData: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Base64url-encoded signature from the authenticator.',
|
||||
})
|
||||
signature: string;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Base64url-encoded user handle, if returned by the authenticator.',
|
||||
required: false,
|
||||
})
|
||||
userHandle?: string;
|
||||
}
|
||||
|
||||
export class PasskeyAssertionRequest {
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Credential ID, as returned by navigator.credentials.get().toJSON().',
|
||||
})
|
||||
id: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Base64url-encoded raw credential ID.',
|
||||
})
|
||||
rawId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Always "public-key".',
|
||||
example: 'public-key',
|
||||
})
|
||||
type: string;
|
||||
|
||||
@ApiProperty({ type: PasskeyAssertionResponseData })
|
||||
response: PasskeyAssertionResponseData;
|
||||
}
|
||||
|
||||
export class PasskeyAllowedCredential {
|
||||
@ApiProperty({
|
||||
description: 'Base64url-encoded credential ID.',
|
||||
})
|
||||
id: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Always "public-key".',
|
||||
example: 'public-key',
|
||||
})
|
||||
type: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Authenticator transports the credential supports.',
|
||||
example: ['internal', 'hybrid'],
|
||||
required: false,
|
||||
})
|
||||
transports?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* WebAuthn request options - pass it to navigator.credentials.get({ publicKey })
|
||||
* on the https://web.whatsapp.com origin.
|
||||
*/
|
||||
export class PasskeyChallenge {
|
||||
@ApiProperty({
|
||||
description: 'Base64url-encoded challenge to sign.',
|
||||
})
|
||||
challenge: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'How long the challenge is valid for, in milliseconds.',
|
||||
example: 60000,
|
||||
})
|
||||
timeout: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Relying party ID.',
|
||||
example: 'web.whatsapp.com',
|
||||
})
|
||||
rpId: string;
|
||||
|
||||
@ApiProperty({ type: [PasskeyAllowedCredential] })
|
||||
allowCredentials: PasskeyAllowedCredential[];
|
||||
|
||||
@ApiProperty({
|
||||
example: 'required',
|
||||
})
|
||||
userVerification: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'WebAuthn extensions requested by WhatsApp.',
|
||||
required: false,
|
||||
})
|
||||
extensions?: Record<string, any>;
|
||||
}
|
||||
|
||||
export class PasskeyConfirmationResponse {
|
||||
@ApiProperty({
|
||||
description:
|
||||
'The code the user must verify against the one shown on their phone.',
|
||||
example: '1234',
|
||||
})
|
||||
code: string;
|
||||
}
|
||||
@@ -45,6 +45,8 @@ export enum WAHASessionStatus {
|
||||
STOPPED = 'STOPPED',
|
||||
STARTING = 'STARTING',
|
||||
SCAN_QR_CODE = 'SCAN_QR_CODE',
|
||||
PASSKEY_REQUIRED = 'PASSKEY_REQUIRED',
|
||||
PASSKEY_CONFIRMATION_REQUIRED = 'PASSKEY_CONFIRMATION_REQUIRED',
|
||||
WORKING = 'WORKING',
|
||||
FAILED = 'FAILED',
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ export class SettingsSecurityChangeInfo {
|
||||
adminsOnly: boolean = true;
|
||||
}
|
||||
|
||||
export class SettingsMemberAddMode {
|
||||
membersCanAddNewMember: boolean = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { applyDecorators } from '@nestjs/common';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { App } from '@waha/apps/app_sdk/dto/app.dto';
|
||||
import { BooleanString } from '@waha/nestjs/validation/BooleanString';
|
||||
import { IsDynamicObject } from '@waha/nestjs/validation/IsDynamicObject';
|
||||
import { SessionName } from '@waha/nestjs/validation/SessionName';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Matches,
|
||||
MaxLength,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
@@ -327,6 +325,56 @@ export class SessionDTO {
|
||||
config?: SessionConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforcement types as listed in the WhatsApp Web app (WAWebUserPrefsTypes.ReachoutTimelockEnforcementType).
|
||||
* WhatsApp may introduce new values at any time - treat it as an open set.
|
||||
*/
|
||||
export enum ReachoutTimelockEnforcementType {
|
||||
// No restriction
|
||||
DEFAULT = 'DEFAULT',
|
||||
BIZ_QUALITY = 'BIZ_QUALITY',
|
||||
BIZ_COMMERCE_VIOLATION_ADULT = 'BIZ_COMMERCE_VIOLATION_ADULT',
|
||||
BIZ_COMMERCE_VIOLATION_ALCOHOL = 'BIZ_COMMERCE_VIOLATION_ALCOHOL',
|
||||
BIZ_COMMERCE_VIOLATION_ANIMALS = 'BIZ_COMMERCE_VIOLATION_ANIMALS',
|
||||
BIZ_COMMERCE_VIOLATION_BODY_PARTS_FLUIDS = 'BIZ_COMMERCE_VIOLATION_BODY_PARTS_FLUIDS',
|
||||
BIZ_COMMERCE_VIOLATION_DATING = 'BIZ_COMMERCE_VIOLATION_DATING',
|
||||
BIZ_COMMERCE_VIOLATION_DIGITAL_SERVICES_PRODUCTS = 'BIZ_COMMERCE_VIOLATION_DIGITAL_SERVICES_PRODUCTS',
|
||||
BIZ_COMMERCE_VIOLATION_DRUGS = 'BIZ_COMMERCE_VIOLATION_DRUGS',
|
||||
BIZ_COMMERCE_VIOLATION_DRUGS_ONLY_OTC = 'BIZ_COMMERCE_VIOLATION_DRUGS_ONLY_OTC',
|
||||
BIZ_COMMERCE_VIOLATION_GAMBLING = 'BIZ_COMMERCE_VIOLATION_GAMBLING',
|
||||
BIZ_COMMERCE_VIOLATION_HEALTHCARE = 'BIZ_COMMERCE_VIOLATION_HEALTHCARE',
|
||||
BIZ_COMMERCE_VIOLATION_REAL_FAKE_CURRENCY = 'BIZ_COMMERCE_VIOLATION_REAL_FAKE_CURRENCY',
|
||||
BIZ_COMMERCE_VIOLATION_SUPPLEMENTS = 'BIZ_COMMERCE_VIOLATION_SUPPLEMENTS',
|
||||
BIZ_COMMERCE_VIOLATION_TOBACCO = 'BIZ_COMMERCE_VIOLATION_TOBACCO',
|
||||
BIZ_COMMERCE_VIOLATION_VIOLENT_CONTENT = 'BIZ_COMMERCE_VIOLATION_VIOLENT_CONTENT',
|
||||
BIZ_COMMERCE_VIOLATION_WEAPONS = 'BIZ_COMMERCE_VIOLATION_WEAPONS',
|
||||
WEB_COMPANION_ONLY = 'WEB_COMPANION_ONLY',
|
||||
RESTRICT_ALL_COMPANIONS = 'RESTRICT_ALL_COMPANIONS',
|
||||
}
|
||||
|
||||
export class ReachoutTimelockData {
|
||||
@ApiProperty({
|
||||
example: ReachoutTimelockEnforcementType.RESTRICT_ALL_COMPANIONS,
|
||||
enum: ReachoutTimelockEnforcementType,
|
||||
description:
|
||||
'Raw WhatsApp enforcement type. Informational only - it does not change what is blocked. ' +
|
||||
'WhatsApp may introduce new values, so treat it as an open set.',
|
||||
})
|
||||
enforcementType: ReachoutTimelockEnforcementType;
|
||||
|
||||
@ApiProperty({
|
||||
example: true,
|
||||
})
|
||||
isActive: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1784477333,
|
||||
nullable: true,
|
||||
description: 'Unix timestamp (seconds) when the enforcement ends.',
|
||||
})
|
||||
timeEnforcementEnds: number | null;
|
||||
}
|
||||
|
||||
export class MeInfo {
|
||||
@ChatIdProperty()
|
||||
id: string;
|
||||
@@ -343,6 +391,15 @@ export class MeInfo {
|
||||
jid?: string;
|
||||
|
||||
pushName: string;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
nullable: true,
|
||||
description:
|
||||
'WhatsApp reachout timelock (account restriction) info. ' +
|
||||
'Null if no enforcement has been seen for the account.',
|
||||
})
|
||||
reachoutTimelock?: ReachoutTimelockData | null;
|
||||
}
|
||||
|
||||
export class SessionInfo extends SessionDTO {
|
||||
@@ -368,20 +425,7 @@ export class SessionDetailedInfo extends SessionInfo {
|
||||
engine?: any;
|
||||
}
|
||||
|
||||
// Affect almost all Databases - Sqlite, MongoDB, Postgres.
|
||||
const DB_NAME_LIMIT = 64;
|
||||
const DB_NAME_MAX_PREFIX_LEN = 'waha_noweb'.length;
|
||||
|
||||
export function SessionName() {
|
||||
return applyDecorators(
|
||||
IsString(),
|
||||
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',
|
||||
}),
|
||||
);
|
||||
}
|
||||
export { SessionName };
|
||||
|
||||
export class SessionCreateRequest {
|
||||
@ApiProperty({
|
||||
|
||||
@@ -125,6 +125,21 @@ export class WASessionStatusBody {
|
||||
status: WAHASessionStatus;
|
||||
|
||||
statuses: SessionStatusPoint[];
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
nullable: true,
|
||||
description:
|
||||
'Extra info that belongs to the current status, null for most of them.\n' +
|
||||
'PASSKEY_REQUIRED - the WebAuthn challenge, ' +
|
||||
'pass it to navigator.credentials.get({ publicKey: data }).\n' +
|
||||
'PASSKEY_CONFIRMATION_REQUIRED - { code } - the code to verify against the phone.\n' +
|
||||
'WORKING - { reachoutTimelock } - present when WhatsApp restricts the account ' +
|
||||
'from messaging new contacts (the cause of 463 errors on send); ' +
|
||||
'the session stays connected and the restriction lifts automatically.',
|
||||
example: null,
|
||||
})
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export class WAHAWebhook<Payload = any> {
|
||||
|
||||
@@ -57,12 +57,47 @@ export async function waitUntil(
|
||||
/**
|
||||
* Handle setTimeout 32-bit overflow
|
||||
*/
|
||||
export function setLongTimeout(callback, delayMs) {
|
||||
const MAX_DELAY = 2_147_483_647; // ~24.8 days
|
||||
export class LongTimeout {
|
||||
private static readonly MAX_DELAY = 2_147_483_647; // ~24.8 days
|
||||
|
||||
if (delayMs > MAX_DELAY) {
|
||||
setTimeout(() => setLongTimeout(callback, delayMs - MAX_DELAY), MAX_DELAY);
|
||||
} else {
|
||||
setTimeout(callback, delayMs);
|
||||
private timeout?: ReturnType<typeof setTimeout>;
|
||||
private unrefed: boolean = false;
|
||||
|
||||
constructor(
|
||||
private callback: () => void,
|
||||
delayMs: number,
|
||||
) {
|
||||
this.schedule(delayMs);
|
||||
}
|
||||
|
||||
private schedule(delayMs: number) {
|
||||
if (delayMs > LongTimeout.MAX_DELAY) {
|
||||
this.timeout = setTimeout(
|
||||
() => this.schedule(delayMs - LongTimeout.MAX_DELAY),
|
||||
LongTimeout.MAX_DELAY,
|
||||
);
|
||||
} else {
|
||||
this.timeout = setTimeout(this.callback, delayMs);
|
||||
}
|
||||
if (this.unrefed) {
|
||||
this.timeout.unref?.();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not keep the process alive just for this timer
|
||||
*/
|
||||
unref(): LongTimeout {
|
||||
this.unrefed = true;
|
||||
this.timeout?.unref?.();
|
||||
return this;
|
||||
}
|
||||
|
||||
clear() {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
}
|
||||
|
||||
export function setLongTimeout(callback: () => void, delayMs: number) {
|
||||
return new LongTimeout(callback, delayMs);
|
||||
}
|
||||
+18
-1
@@ -1,5 +1,15 @@
|
||||
/**
|
||||
* If value is ms - we convert it to seconds
|
||||
* Both helpers detect the unit by magnitude, using 1e12 as the threshold:
|
||||
* as unix SECONDS 1e12 is year 33658, as unix MILLISECONDS it's Sep 2001 - so any realistic "now-ish"
|
||||
* timestamp is below 1e12 in seconds and above it in milliseconds.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Normalize a unix timestamp to SECONDS.
|
||||
* Milliseconds get converted (floored), values already in seconds pass through unchanged.
|
||||
*
|
||||
* EnsureSeconds(1784477333) => 1784477333 (already seconds)
|
||||
* EnsureSeconds(1784477333000) => 1784477333 (ms => seconds)
|
||||
*/
|
||||
export function EnsureSeconds(ms: number) {
|
||||
if (!ms) {
|
||||
@@ -10,6 +20,13 @@ export function EnsureSeconds(ms: number) {
|
||||
}
|
||||
return ms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a unix timestamp to MILLISECONDS. Seconds get converted, values already in milliseconds pass through unchanged.
|
||||
*
|
||||
* EnsureMilliseconds(1784477333) => 1784477333000 (seconds => ms)
|
||||
* EnsureMilliseconds(1784477333000) => 1784477333000 (already ms)
|
||||
*/
|
||||
export function EnsureMilliseconds(seconds: number) {
|
||||
if (!seconds) {
|
||||
return seconds;
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ function getPlatform() {
|
||||
}
|
||||
|
||||
export const VERSION: WAHAEnvironment = {
|
||||
version: '2026.6.1',
|
||||
version: '2026.7.2',
|
||||
engine: getEngineName(),
|
||||
tier: getWAHAVersion(),
|
||||
browser: getBrowser(),
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"waha": {
|
||||
"gows": {
|
||||
"repo": "devlikeapro/gows",
|
||||
"ref": "v1.0.41"
|
||||
"repo": "devlikeapro/gows-plus",
|
||||
"ref": "v1.0.44"
|
||||
},
|
||||
"dashboard": {
|
||||
"repo": "devlikeapro/dashboard",
|
||||
"ref": "47244c7d9202e0f4c1b23d12d45d5c208cd1184f"
|
||||
"ref": "d670e01a3776d94842c230f94fab8ebce833f8d5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@
|
||||
# Manual changes might be lost - proceed with caution!
|
||||
|
||||
__metadata:
|
||||
version: 8
|
||||
version: 10
|
||||
cacheKey: 10
|
||||
|
||||
"@adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2026-04-28":
|
||||
version: 7.0.0-rc13
|
||||
resolution: "@adiwajshing/baileys@https://github.com/devlikeapro/Baileys.git#commit=f619b7e841450140e444a0e93ff3f04ab50519b6"
|
||||
version: 7.0.0-rc14
|
||||
resolution: "@adiwajshing/baileys@https://github.com/devlikeapro/Baileys.git#commit=0124c8f073719727fb8db2b08d4b14b81573d82c"
|
||||
dependencies:
|
||||
"@cacheable/node-cache": "npm:^1.4.0"
|
||||
"@hapi/boom": "npm:^9.1.3"
|
||||
@@ -32,7 +32,7 @@ __metadata:
|
||||
optional: true
|
||||
link-preview-js:
|
||||
optional: true
|
||||
checksum: 10/535e52bf0b5bad381c985fcead24cb8c262a97662b9aaf3a4c0081c47ba02204e9f015e96dfb043f9d8e75295498e3e7468ee056f82f832af882a321c475d7bd
|
||||
checksum: 10/35b77766d9a8eccc8366efa5a0f702b45039df3578b9ac6bc53743305255b9c7f28ca04248c50f30ed7af3d5adcd4842fdeeb557967f21d7dc096c303de8cf55
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1596,13 +1596,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-win32-x64@npm:0.34.5, @img/sharp-win32-x64@npm:^0.34.5":
|
||||
"@img/sharp-win32-x64@npm:0.34.5":
|
||||
version: 0.34.5
|
||||
resolution: "@img/sharp-win32-x64@npm:0.34.5"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-win32-x64@npm:^0.35.3":
|
||||
version: 0.35.3
|
||||
resolution: "@img/sharp-win32-x64@npm:0.35.3"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@inquirer/checkbox@npm:^4.1.2, @inquirer/checkbox@npm:^4.1.5":
|
||||
version: 4.1.8
|
||||
resolution: "@inquirer/checkbox@npm:4.1.8"
|
||||
@@ -4617,15 +4624,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@wppconnect-team/wppconnect@npm:^2.2.1":
|
||||
version: 2.2.1
|
||||
resolution: "@wppconnect-team/wppconnect@npm:2.2.1"
|
||||
"@wppconnect-team/wppconnect@github:wppconnect-team/wppconnect#master":
|
||||
version: 2.2.3
|
||||
resolution: "@wppconnect-team/wppconnect@https://github.com/wppconnect-team/wppconnect.git#commit=b467df8a5e0b00bbd794b5afc40c7fb1836e70a0"
|
||||
dependencies:
|
||||
"@img/sharp-win32-x64": "npm:^0.34.5"
|
||||
"@wppconnect/wa-js": "npm:^4.3.0"
|
||||
"@img/sharp-win32-x64": "npm:^0.35.3"
|
||||
"@wppconnect/wa-js": "npm:^4.4.1"
|
||||
"@wppconnect/wa-version": "npm:^1.5.3941"
|
||||
atob: "npm:^2.1.2"
|
||||
axios: "npm:^1.16.1"
|
||||
axios: "npm:^1.18.1"
|
||||
boxen: "npm:^5.1.2"
|
||||
catch-exit: "npm:^2.0.0"
|
||||
chalk: "npm:~4.1.2"
|
||||
@@ -4648,7 +4655,7 @@ __metadata:
|
||||
rimraf: "npm:^3.0.2"
|
||||
sanitize-filename: "npm:^1.6.4"
|
||||
sharp: "npm:0.34.5"
|
||||
tmp: "npm:^0.2.5"
|
||||
tmp: "npm:^0.2.7"
|
||||
tree-kill: "npm:^1.2.2"
|
||||
winston: "npm:^3.19.0"
|
||||
ws: "npm:^8.21.0"
|
||||
@@ -4657,14 +4664,21 @@ __metadata:
|
||||
optional: true
|
||||
fsevents:
|
||||
optional: true
|
||||
checksum: 10/d7940928c2874ddd0e2412b0628d2750c2457bf9b08a8840e79de24e8666f792e2e7a78c0bc16c13c4ab870e7d37683ad1b08522fb9ea7da90f695e5b21cc044
|
||||
checksum: 10/73baacfc22b9cbc40cbec4d010591b6614e28e7e5d95be0d8cbd378d8160f886cd2683c12a0ad8bbe8e073d8bbbc5d0cb785e316320e60e6e0178c36af779a99
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@wppconnect/wa-js@npm:^4.3.0":
|
||||
version: 4.3.0
|
||||
resolution: "@wppconnect/wa-js@npm:4.3.0"
|
||||
checksum: 10/844e63b58b62d4057ab51f114b3f9b12e13637a026b6df50c5f53801181a3800065342c0f79478db10d43dfe96a6ec60bf9d8fab7840caaf94970a774572279d
|
||||
"@wppconnect/wa-js@github:wppconnect-team/wa-js#main":
|
||||
version: 4.4.1
|
||||
resolution: "@wppconnect/wa-js@https://github.com/wppconnect-team/wa-js.git#commit=1f7cc7e12e2cf9246a65bb8de4348055016f374e"
|
||||
checksum: 10/8c6a370cfa6d4ec516664b71fd1b0e4300f3dc28433925cace8df7954c6b1781913c9993d573cb39cb78d363b3edf2f4d23255655d4cdd053296ffe4534c1170
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@wppconnect/wa-js@npm:^4.4.1":
|
||||
version: 4.4.1
|
||||
resolution: "@wppconnect/wa-js@npm:4.4.1"
|
||||
checksum: 10/8c6a370cfa6d4ec516664b71fd1b0e4300f3dc28433925cace8df7954c6b1781913c9993d573cb39cb78d363b3edf2f4d23255655d4cdd053296ffe4534c1170
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -4971,54 +4985,33 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"archiver-utils@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "archiver-utils@npm:2.1.0"
|
||||
"archiver-utils@npm:^5.0.0, archiver-utils@npm:^5.0.2":
|
||||
version: 5.0.2
|
||||
resolution: "archiver-utils@npm:5.0.2"
|
||||
dependencies:
|
||||
glob: "npm:^7.1.4"
|
||||
glob: "npm:^10.0.0"
|
||||
graceful-fs: "npm:^4.2.0"
|
||||
is-stream: "npm:^2.0.1"
|
||||
lazystream: "npm:^1.0.0"
|
||||
lodash.defaults: "npm:^4.2.0"
|
||||
lodash.difference: "npm:^4.5.0"
|
||||
lodash.flatten: "npm:^4.4.0"
|
||||
lodash.isplainobject: "npm:^4.0.6"
|
||||
lodash.union: "npm:^4.6.0"
|
||||
lodash: "npm:^4.17.15"
|
||||
normalize-path: "npm:^3.0.0"
|
||||
readable-stream: "npm:^2.0.0"
|
||||
checksum: 10/4df493c0e6a3a544119b08b350308923500e2c6efee6a283cba4c3202293ce3acb70897e54e24f735e3a38ff43e5a65f66e2e5225fdfc955bf2335491377be2e
|
||||
readable-stream: "npm:^4.0.0"
|
||||
checksum: 10/9dde4aa3f0cb1bdfe0b3d4c969f82e6cca9ae76338b7fee6f0071a14a2a38c0cdd1c41ecd3e362466585aa6cc5d07e9e435abea8c94fd9c7ace35f184abef9e4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"archiver-utils@npm:^3.0.4":
|
||||
version: 3.0.4
|
||||
resolution: "archiver-utils@npm:3.0.4"
|
||||
"archiver@npm:7.0.1":
|
||||
version: 7.0.1
|
||||
resolution: "archiver@npm:7.0.1"
|
||||
dependencies:
|
||||
glob: "npm:^7.2.3"
|
||||
graceful-fs: "npm:^4.2.0"
|
||||
lazystream: "npm:^1.0.0"
|
||||
lodash.defaults: "npm:^4.2.0"
|
||||
lodash.difference: "npm:^4.5.0"
|
||||
lodash.flatten: "npm:^4.4.0"
|
||||
lodash.isplainobject: "npm:^4.0.6"
|
||||
lodash.union: "npm:^4.6.0"
|
||||
normalize-path: "npm:^3.0.0"
|
||||
readable-stream: "npm:^3.6.0"
|
||||
checksum: 10/a838c325a1e1d6798c07e6a3af08f480fdce57cba2964bff8761126715aa1b71e9a119442eac19b7ec6313f5298e54a180dc6612ae548825fbc9be6836e50487
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"archiver@npm:^5.3.1":
|
||||
version: 5.3.2
|
||||
resolution: "archiver@npm:5.3.2"
|
||||
dependencies:
|
||||
archiver-utils: "npm:^2.1.0"
|
||||
archiver-utils: "npm:^5.0.2"
|
||||
async: "npm:^3.2.4"
|
||||
buffer-crc32: "npm:^0.2.1"
|
||||
readable-stream: "npm:^3.6.0"
|
||||
buffer-crc32: "npm:^1.0.0"
|
||||
readable-stream: "npm:^4.0.0"
|
||||
readdir-glob: "npm:^1.1.2"
|
||||
tar-stream: "npm:^2.2.0"
|
||||
zip-stream: "npm:^4.1.0"
|
||||
checksum: 10/9384b3b20d330f95140c2b7a9b51140d14e9bc7b133be6cf573067ed8fc67a6e9618cfbfe60b1ba78b8034857001fd02c8900f2fba4864514670a2274d36dc9e
|
||||
tar-stream: "npm:^3.0.0"
|
||||
zip-stream: "npm:^6.0.1"
|
||||
checksum: 10/81c6102db99d7ffd5cb2aed02a678f551c6603991a059ca66ef59249942b835a651a3d3b5240af4f8bec4e61e13790357c9d1ad4a99982bd2cc4149575c31d67
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5309,6 +5302,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bare-fs@npm:^4.5.5":
|
||||
version: 4.7.2
|
||||
resolution: "bare-fs@npm:4.7.2"
|
||||
dependencies:
|
||||
bare-events: "npm:^2.5.4"
|
||||
bare-path: "npm:^3.0.0"
|
||||
bare-stream: "npm:^2.6.4"
|
||||
bare-url: "npm:^2.2.2"
|
||||
fast-fifo: "npm:^1.3.2"
|
||||
peerDependencies:
|
||||
bare-buffer: "*"
|
||||
peerDependenciesMeta:
|
||||
bare-buffer:
|
||||
optional: true
|
||||
checksum: 10/a0fafaf46e6a3b77e7d36ff34f1f71e417d548c4ebac90238a0e42376b8efa1698f820133fbf8a09f6b6cb14cbd19f782d9c8bb8263cdf3c8e55e27ad2c1d648
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bare-os@npm:^3.0.1":
|
||||
version: 3.6.1
|
||||
resolution: "bare-os@npm:3.6.1"
|
||||
@@ -5342,6 +5353,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bare-url@npm:^2.2.2":
|
||||
version: 2.4.5
|
||||
resolution: "bare-url@npm:2.4.5"
|
||||
dependencies:
|
||||
bare-path: "npm:^3.0.0"
|
||||
checksum: 10/65d7a906dae5051ba37d8b96fd3cd1ba8b8e1a7f74340a363c6b45123908d707a4d58c4e21c97f6735c4dfcd6c499d353a67cc8a4ff638d4e8b46a1d9984ad60
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"base64-js@npm:^1.3.1":
|
||||
version: 1.5.1
|
||||
resolution: "base64-js@npm:1.5.1"
|
||||
@@ -5376,13 +5396,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"big-integer@npm:^1.6.17":
|
||||
version: 1.6.52
|
||||
resolution: "big-integer@npm:1.6.52"
|
||||
checksum: 10/4bc6ae152a96edc9f95020f5fc66b13d26a9ad9a021225a9f0213f7e3dc44269f423aa8c42e19d6ac4a63bb2b22140b95d10be8f9ca7a6d9aa1b22b330d1f514
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"big.js@npm:^5.2.2":
|
||||
version: 5.2.2
|
||||
resolution: "big.js@npm:5.2.2"
|
||||
@@ -5397,16 +5410,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"binary@npm:~0.3.0":
|
||||
version: 0.3.0
|
||||
resolution: "binary@npm:0.3.0"
|
||||
dependencies:
|
||||
buffers: "npm:~0.1.1"
|
||||
chainsaw: "npm:~0.1.0"
|
||||
checksum: 10/127591ebb7bfca242ec11be9ef874bcde17c520f249d764810045971b6617b659e8af4452f8a1586db7fd47e1b481a75d22c8f207fc1466c0f099b9435e51679
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bindings@npm:^1.5.0":
|
||||
version: 1.5.0
|
||||
resolution: "bindings@npm:1.5.0"
|
||||
@@ -5427,10 +5430,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bluebird@npm:~3.4.1":
|
||||
version: 3.4.7
|
||||
resolution: "bluebird@npm:3.4.7"
|
||||
checksum: 10/340e4d11d4b6a26d90371180effb4e500197c2943e5426472d6b6bffca0032a534226ad10255fc0e39c025bea197341c6b2a4258f8c0f18217c7b3a254c76c14
|
||||
"bluebird@npm:~3.7.2":
|
||||
version: 3.7.2
|
||||
resolution: "bluebird@npm:3.7.2"
|
||||
checksum: 10/007c7bad22c5d799c8dd49c85b47d012a1fe3045be57447721e6afbd1d5be43237af1db62e26cb9b0d9ba812d2e4ca3bac82f6d7e016b6b88de06ee25ceb96e7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5565,7 +5568,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffer-crc32@npm:^0.2.1, buffer-crc32@npm:^0.2.13, buffer-crc32@npm:~0.2.3":
|
||||
"buffer-crc32@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "buffer-crc32@npm:1.0.0"
|
||||
checksum: 10/ef3b7c07622435085c04300c9a51e850ec34a27b2445f758eef69b859c7827848c2282f3840ca6c1eef3829145a1580ce540cab03ccf4433827a2b95d3b09ca7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffer-crc32@npm:~0.2.3":
|
||||
version: 0.2.13
|
||||
resolution: "buffer-crc32@npm:0.2.13"
|
||||
checksum: 10/06252347ae6daca3453b94e4b2f1d3754a3b146a111d81c68924c22d91889a40623264e95e67955b1cb4a68cbedf317abeabb5140a9766ed248973096db5ce1c
|
||||
@@ -5579,13 +5589,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffer-indexof-polyfill@npm:~1.0.0":
|
||||
version: 1.0.2
|
||||
resolution: "buffer-indexof-polyfill@npm:1.0.2"
|
||||
checksum: 10/808c58a3f06cc6ee2231060959eaa31c490248465f2847e8cfebd3e62563521e67346391caad03ce7616fd765374eb53e941bdd22edb2336431171f46fddcd89
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffer@npm:^5.5.0":
|
||||
version: 5.7.1
|
||||
resolution: "buffer@npm:5.7.1"
|
||||
@@ -5606,13 +5609,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffers@npm:~0.1.1":
|
||||
version: 0.1.1
|
||||
resolution: "buffers@npm:0.1.1"
|
||||
checksum: 10/9f0b64bbb8ac4783b1740219ab3532b03ef978fa38e70a0ba8c0695a2f6bc7e2af0ce42f0756b0c1a127070493055adbaf490fb68d95bebd7ccc310c6a483860
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bufferutil@npm:^4.0.8":
|
||||
version: 4.0.9
|
||||
resolution: "bufferutil@npm:4.0.9"
|
||||
@@ -5782,15 +5778,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chainsaw@npm:~0.1.0":
|
||||
version: 0.1.0
|
||||
resolution: "chainsaw@npm:0.1.0"
|
||||
dependencies:
|
||||
traverse: "npm:>=0.3.0 <0.4"
|
||||
checksum: 10/d85627cd3440eb908b9cd72a1ddce4a36bb1ebc9d431a4a2f44b4435cbefdd83625c05114d870381ba765849c34ad05f236c3f590b1581ea03c22897fe6883d0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chalk@npm:^2.3.0":
|
||||
version: 2.4.2
|
||||
resolution: "chalk@npm:2.4.2"
|
||||
@@ -6270,15 +6257,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"compress-commons@npm:^4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "compress-commons@npm:4.1.2"
|
||||
"compress-commons@npm:^6.0.2":
|
||||
version: 6.0.2
|
||||
resolution: "compress-commons@npm:6.0.2"
|
||||
dependencies:
|
||||
buffer-crc32: "npm:^0.2.13"
|
||||
crc32-stream: "npm:^4.0.2"
|
||||
crc-32: "npm:^1.2.0"
|
||||
crc32-stream: "npm:^6.0.0"
|
||||
is-stream: "npm:^2.0.1"
|
||||
normalize-path: "npm:^3.0.0"
|
||||
readable-stream: "npm:^3.6.0"
|
||||
checksum: 10/76fa281412e4a95f89893dc1e3399e797de20253365cf53102ac4738fa004d3540abb12c26e3a54156f8fb4e4392ef9a9c5eecbe752f3a7d30e28c808b671e1b
|
||||
readable-stream: "npm:^4.0.0"
|
||||
checksum: 10/78e3ba10aeef919a1c5bbac21e120f3e1558a31b2defebbfa1635274fc7f7e8a3a0ee748a06249589acd0b33a0d58144b8238ff77afc3220f8d403a96fcc13aa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -6429,13 +6417,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"crc32-stream@npm:^4.0.2":
|
||||
version: 4.0.3
|
||||
resolution: "crc32-stream@npm:4.0.3"
|
||||
"crc32-stream@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "crc32-stream@npm:6.0.0"
|
||||
dependencies:
|
||||
crc-32: "npm:^1.2.0"
|
||||
readable-stream: "npm:^3.4.0"
|
||||
checksum: 10/d44d0ec6f04d8a1bed899ac3e4fbb82111ed567ea6d506be39147362af45c747887fce1032f4beca1646b4824e5a9614cd3332bfa94bbc5577ca5445e7f75ddd
|
||||
readable-stream: "npm:^4.0.0"
|
||||
checksum: 10/e6edc2f81bc387daef6d18b2ac18c2ffcb01b554d3b5c7d8d29b177505aafffba574658fdd23922767e8dab1183d1962026c98c17e17fb272794c33293ef607c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7734,7 +7722,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fs-extra@npm:^10.0.0, fs-extra@npm:^10.1.0":
|
||||
"fs-extra@npm:11.3.4":
|
||||
version: 11.3.4
|
||||
resolution: "fs-extra@npm:11.3.4"
|
||||
dependencies:
|
||||
graceful-fs: "npm:^4.2.0"
|
||||
jsonfile: "npm:^6.0.1"
|
||||
universalify: "npm:^2.0.0"
|
||||
checksum: 10/1b8deea9c540a2efe63c750bc9e1ba6238115579d1571d67fe8fb58e3fb6df19aba29fd4ebb81217cf0bf5bce0df30ca68dbc3e06f6652b856edd385ce0ff649
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fs-extra@npm:^10.0.0":
|
||||
version: 10.1.0
|
||||
resolution: "fs-extra@npm:10.1.0"
|
||||
dependencies:
|
||||
@@ -7807,18 +7806,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fstream@npm:^1.0.12":
|
||||
version: 1.0.12
|
||||
resolution: "fstream@npm:1.0.12"
|
||||
dependencies:
|
||||
graceful-fs: "npm:^4.1.2"
|
||||
inherits: "npm:~2.0.0"
|
||||
mkdirp: "npm:>=0.5 0"
|
||||
rimraf: "npm:2"
|
||||
checksum: 10/eadba4375e952f3f7e9d34d822cfa1592134173033bafef42aa23d5f09bf373e4eb77e097883c0a9136ad7e7d3b49bb14f0e8dfaa489abd5139b5a3c961787b6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"function-bind@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "function-bind@npm:1.1.2"
|
||||
@@ -7997,6 +7984,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:^10.0.0":
|
||||
version: 10.5.0
|
||||
resolution: "glob@npm:10.5.0"
|
||||
dependencies:
|
||||
foreground-child: "npm:^3.1.0"
|
||||
jackspeak: "npm:^3.1.2"
|
||||
minimatch: "npm:^9.0.4"
|
||||
minipass: "npm:^7.1.2"
|
||||
package-json-from-dist: "npm:^1.0.0"
|
||||
path-scurry: "npm:^1.11.1"
|
||||
bin:
|
||||
glob: dist/esm/bin.mjs
|
||||
checksum: 10/ab3bccfefcc0afaedbd1f480cd0c4a2c0e322eb3f0aa7ceaa31b3f00b825069f17cf0f1fc8b6f256795074b903f37c0ade37ddda6a176aa57f1c2bbfe7240653
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:^10.2.2":
|
||||
version: 10.4.5
|
||||
resolution: "glob@npm:10.4.5"
|
||||
@@ -8013,7 +8016,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.2.3":
|
||||
"glob@npm:^7.1.3, glob@npm:^7.1.4":
|
||||
version: 7.2.3
|
||||
resolution: "glob@npm:7.2.3"
|
||||
dependencies:
|
||||
@@ -8403,7 +8406,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.0, inherits@npm:~2.0.3, inherits@npm:~2.0.4":
|
||||
"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3, inherits@npm:~2.0.4":
|
||||
version: 2.0.4
|
||||
resolution: "inherits@npm:2.0.4"
|
||||
checksum: 10/cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521
|
||||
@@ -8601,7 +8604,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-stream@npm:^2.0.0":
|
||||
"is-stream@npm:^2.0.0, is-stream@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "is-stream@npm:2.0.1"
|
||||
checksum: 10/b8e05ccdf96ac330ea83c12450304d4a591f9958c11fd17bed240af8d5ffe08aedafa4c0f4cfccd4d28dc9d4d129daca1023633d5c11601a6cbc77521f6fae66
|
||||
@@ -9559,13 +9562,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"listenercount@npm:~1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "listenercount@npm:1.0.1"
|
||||
checksum: 10/208c6d2b57dc16c22cc71b58a7debb6f4612a79de211b76e251efee8eb03b9f6acd4651399016ef9c15ff6a3dedfd7acc96064acddce0dbe627e2d8478034d3d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"load-esm@npm:1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "load-esm@npm:1.0.2"
|
||||
@@ -9621,20 +9617,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.difference@npm:^4.5.0":
|
||||
version: 4.5.0
|
||||
resolution: "lodash.difference@npm:4.5.0"
|
||||
checksum: 10/b22adb1be9c60e5997b8b483f8bab19878cb40eda65437907958e5d27990214716e1b00ebe312a97f47e63d8b891e4ae30947d08e1f0861ccdb9462f56ab9d77
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.flatten@npm:^4.4.0":
|
||||
version: 4.4.0
|
||||
resolution: "lodash.flatten@npm:4.4.0"
|
||||
checksum: 10/a2b192f220b0b6c78a6c0175e96bad888b9e0f2a887a8e8c1d0c29d03231fbf110bbb9be0d9de5f936537d143eeb9d5b4f44c4a44f5592c195bf2fae6a6b1e3a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.isarguments@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "lodash.isarguments@npm:3.1.0"
|
||||
@@ -9642,13 +9624,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.isplainobject@npm:^4.0.6":
|
||||
version: 4.0.6
|
||||
resolution: "lodash.isplainobject@npm:4.0.6"
|
||||
checksum: 10/29c6351f281e0d9a1d58f1a4c8f4400924b4c79f18dfc4613624d7d54784df07efaff97c1ff2659f3e085ecf4fff493300adc4837553104cef2634110b0d5337
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.memoize@npm:^4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "lodash.memoize@npm:4.1.2"
|
||||
@@ -9656,13 +9631,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.union@npm:^4.6.0":
|
||||
version: 4.6.0
|
||||
resolution: "lodash.union@npm:4.6.0"
|
||||
checksum: 10/175f5786efc527238c1350ce561c28e5ba527b5957605f9e5b8a804fce78801d09ced7b72de0302325e5b14c711f94690b1a733c13ad3674cc1a76e1172db1f8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash@npm:4.17.21, lodash@npm:^4.17.11, lodash@npm:^4.17.15, lodash@npm:^4.17.21":
|
||||
version: 4.17.21
|
||||
resolution: "lodash@npm:4.17.21"
|
||||
@@ -10014,6 +9982,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mime@npm:3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "mime@npm:3.0.0"
|
||||
bin:
|
||||
mime: cli.js
|
||||
checksum: 10/b2d31580deb58be89adaa1877cbbf152b7604b980fd7ef8f08b9e96bfedf7d605d9c23a8ba62aa12c8580b910cd7c1d27b7331d0f40f7a14e17d5a0bbec3b49f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mime@npm:^1.4.1":
|
||||
version: 1.6.0
|
||||
resolution: "mime@npm:1.6.0"
|
||||
@@ -10023,15 +10000,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mime@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "mime@npm:3.0.0"
|
||||
bin:
|
||||
mime: cli.js
|
||||
checksum: 10/b2d31580deb58be89adaa1877cbbf152b7604b980fd7ef8f08b9e96bfedf7d605d9c23a8ba62aa12c8580b910cd7c1d27b7331d0f40f7a14e17d5a0bbec3b49f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mimic-fn@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "mimic-fn@npm:2.1.0"
|
||||
@@ -10253,7 +10221,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mkdirp@npm:>=0.5 0, mkdirp@npm:^0.5.6":
|
||||
"mkdirp@npm:^0.5.6":
|
||||
version: 0.5.6
|
||||
resolution: "mkdirp@npm:0.5.6"
|
||||
dependencies:
|
||||
@@ -10545,7 +10513,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9, node-fetch@npm:^2.7.0":
|
||||
"node-fetch@npm:2.7.0, node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0":
|
||||
version: 2.7.0
|
||||
resolution: "node-fetch@npm:2.7.0"
|
||||
dependencies:
|
||||
@@ -10644,14 +10612,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-webpmux@npm:3.1.7":
|
||||
version: 3.1.7
|
||||
resolution: "node-webpmux@npm:3.1.7"
|
||||
checksum: 10/2699d6fef59b6641112373bfeabffa39085d08b0dc9da16bc44d5a4878ca2c6ef17da1286764dc40f8f9094a9e775c20b5009d7e4b72cf8910037c8fd4495495
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-webpmux@npm:^3.2.1":
|
||||
"node-webpmux@npm:3.2.1, node-webpmux@npm:^3.2.1":
|
||||
version: 3.2.1
|
||||
resolution: "node-webpmux@npm:3.2.1"
|
||||
checksum: 10/8797ebe4846707443629800c70d95c592ab1c18f313b360dd26832b8443efa9b89966269ad6b1e3aa59a18e790406b29a55e19c10212fc3068dc476e48cee6e9
|
||||
@@ -12005,7 +11966,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.3.5, readable-stream@npm:~2.3.6":
|
||||
"readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.3.5":
|
||||
version: 2.3.8
|
||||
resolution: "readable-stream@npm:2.3.8"
|
||||
dependencies:
|
||||
@@ -12224,14 +12185,14 @@ __metadata:
|
||||
|
||||
"resolve@patch:resolve@npm%3A^1.20.0#optional!builtin<compat/resolve>":
|
||||
version: 1.22.10
|
||||
resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin<compat/resolve>::version=1.22.10&hash=c3c19d"
|
||||
resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin<compat/resolve>::version=1.22.10&hash=9bd1a5"
|
||||
dependencies:
|
||||
is-core-module: "npm:^2.16.0"
|
||||
path-parse: "npm:^1.0.7"
|
||||
supports-preserve-symlinks-flag: "npm:^1.0.0"
|
||||
bin:
|
||||
resolve: bin/resolve
|
||||
checksum: 10/d4d878bfe3702d215ea23e75e0e9caf99468e3db76f5ca100d27ebdc527366fee3877e54bce7d47cc72ca8952fc2782a070d238bfa79a550eeb0082384c3b81a
|
||||
checksum: 10/c202d86f0b7090886c41427c7161c781169fafe20c9389c5feb0bc021a83e29db8f691c02fcd7d9730db2e7de6ef608a45d0e509733e7e54060d64f0e38521a5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -12268,17 +12229,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rimraf@npm:2":
|
||||
version: 2.7.1
|
||||
resolution: "rimraf@npm:2.7.1"
|
||||
dependencies:
|
||||
glob: "npm:^7.1.3"
|
||||
bin:
|
||||
rimraf: ./bin.js
|
||||
checksum: 10/4586c296c736483e297da7cffd19475e4a3e41d07b1ae124aad5d687c79e4ffa716bdac8732ed1db942caf65271cee9dd39f8b639611de161a2753e2112ffe1d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rimraf@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "rimraf@npm:3.0.2"
|
||||
@@ -12491,13 +12441,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"setimmediate@npm:~1.0.4":
|
||||
version: 1.0.5
|
||||
resolution: "setimmediate@npm:1.0.5"
|
||||
checksum: 10/76e3f5d7f4b581b6100ff819761f04a984fa3f3990e72a6554b57188ded53efce2d3d6c0932c10f810b7c59414f85e2ab3c11521877d1dea1ce0b56dc906f485
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"setprototypeof@npm:1.2.0, setprototypeof@npm:~1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "setprototypeof@npm:1.2.0"
|
||||
@@ -13228,7 +13171,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar-stream@npm:^2.1.4, tar-stream@npm:^2.2.0":
|
||||
"tar-stream@npm:^2.1.4":
|
||||
version: 2.2.0
|
||||
resolution: "tar-stream@npm:2.2.0"
|
||||
dependencies:
|
||||
@@ -13241,6 +13184,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar-stream@npm:^3.0.0":
|
||||
version: 3.2.0
|
||||
resolution: "tar-stream@npm:3.2.0"
|
||||
dependencies:
|
||||
b4a: "npm:^1.6.4"
|
||||
bare-fs: "npm:^4.5.5"
|
||||
fast-fifo: "npm:^1.2.0"
|
||||
streamx: "npm:^2.15.0"
|
||||
checksum: 10/ce57a81521de73ae7a3b7d55a08da50d6771427c249bfa89a208518e48faf5254c8fa7201a8f5419ab8bde9601a74e6dd512b31a13ec89774aec96178f99a8d3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar-stream@npm:^3.1.5":
|
||||
version: 3.1.7
|
||||
resolution: "tar-stream@npm:3.1.7"
|
||||
@@ -13385,10 +13340,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tmp@npm:^0.2.5":
|
||||
version: 0.2.5
|
||||
resolution: "tmp@npm:0.2.5"
|
||||
checksum: 10/dd4b78b32385eab4899d3ae296007b34482b035b6d73e1201c4a9aede40860e90997a1452c65a2d21aee73d53e93cd167d741c3db4015d90e63b6d568a93d7ec
|
||||
"tmp@npm:^0.2.7":
|
||||
version: 0.2.7
|
||||
resolution: "tmp@npm:0.2.7"
|
||||
checksum: 10/0a3bc90beb0c6275273c3475fb57e466eaab1c9c4a101d029ff62b18146ce136e7f75d09de34863d9f2c2a492751402508f9e028bc98eb34a1416195d4b15619
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -13473,13 +13428,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"traverse@npm:>=0.3.0 <0.4":
|
||||
version: 0.3.9
|
||||
resolution: "traverse@npm:0.3.9"
|
||||
checksum: 10/ffbb8460a934f271b7b7ae654e676f740d81037d6c20ab9fd05781cfdf644929f494399b5cb3aa3db4ab69cbfef06ff8f885560d523ca49b7da33763f6c4c9f1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tree-kill@npm:1.2.2, tree-kill@npm:^1.2.2":
|
||||
version: 1.2.2
|
||||
resolution: "tree-kill@npm:1.2.2"
|
||||
@@ -13844,21 +13792,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unzipper@npm:^0.10.11":
|
||||
version: 0.10.14
|
||||
resolution: "unzipper@npm:0.10.14"
|
||||
"unzipper@npm:0.12.3":
|
||||
version: 0.12.3
|
||||
resolution: "unzipper@npm:0.12.3"
|
||||
dependencies:
|
||||
big-integer: "npm:^1.6.17"
|
||||
binary: "npm:~0.3.0"
|
||||
bluebird: "npm:~3.4.1"
|
||||
buffer-indexof-polyfill: "npm:~1.0.0"
|
||||
bluebird: "npm:~3.7.2"
|
||||
duplexer2: "npm:~0.1.4"
|
||||
fstream: "npm:^1.0.12"
|
||||
fs-extra: "npm:^11.2.0"
|
||||
graceful-fs: "npm:^4.2.2"
|
||||
listenercount: "npm:~1.0.1"
|
||||
readable-stream: "npm:~2.3.6"
|
||||
setimmediate: "npm:~1.0.4"
|
||||
checksum: 10/3f7b44f3c7253bc08da2988baf559f00b261c5340625e6e5206c5d73b4dea409b89caae4048346cf9f215d3cdf930e3bdee98edac5e0abc843eed765c52b398d
|
||||
node-int64: "npm:^0.4.0"
|
||||
checksum: 10/b210c421308e1913e01b54faad4ae79e758c674311892414a0697acacba9f82fa0051b677faa77e62fab422eef928c858f2d5cda9ddb47a2f3db95b0e9b36359
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -14022,8 +13965,8 @@ __metadata:
|
||||
"@types/supertest": "npm:^2.0.8"
|
||||
"@types/user-agents": "npm:^1"
|
||||
"@types/ws": "npm:^8.5.4"
|
||||
"@wppconnect-team/wppconnect": "npm:^2.2.1"
|
||||
"@wppconnect/wa-js": "npm:^4.3.0"
|
||||
"@wppconnect-team/wppconnect": "github:wppconnect-team/wppconnect#master"
|
||||
"@wppconnect/wa-js": "github:wppconnect-team/wa-js#main"
|
||||
adm-zip: "npm:0.5.10"
|
||||
agentkeepalive: "npm:^4.5.0"
|
||||
async-lock: "npm:^1.4.1"
|
||||
@@ -14094,7 +14037,7 @@ __metadata:
|
||||
undici: "npm:^7.16.0"
|
||||
uniqid: "npm:^5.4.0"
|
||||
user-agents: "npm:^1.1.669"
|
||||
whatsapp-web.js: "github:devlikeapro/whatsapp-web.js#fork-main-2026-02-18"
|
||||
whatsapp-web.js: "github:devlikeapro/whatsapp-web.js#fork-main-2026-06-26"
|
||||
write-file-atomic: "npm:^6.0.0"
|
||||
yaml: "npm:^2.7.1"
|
||||
zod: "npm:^4.3.6"
|
||||
@@ -14203,18 +14146,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2026-02-18":
|
||||
version: 1.34.6
|
||||
resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=d9f107887470bd4349cd9f639fb9cb1d63027efc"
|
||||
"whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2026-06-26":
|
||||
version: 1.34.7
|
||||
resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=23d28e7070e9e2689280118c184edb8cb3bb709b"
|
||||
dependencies:
|
||||
archiver: "npm:^5.3.1"
|
||||
archiver: "npm:7.0.1"
|
||||
fluent-ffmpeg: "npm:2.1.3"
|
||||
fs-extra: "npm:^10.1.0"
|
||||
mime: "npm:^3.0.0"
|
||||
node-fetch: "npm:^2.6.9"
|
||||
node-webpmux: "npm:3.1.7"
|
||||
puppeteer: "npm:^24.31.0"
|
||||
unzipper: "npm:^0.10.11"
|
||||
fs-extra: "npm:11.3.4"
|
||||
mime: "npm:3.0.0"
|
||||
node-fetch: "npm:2.7.0"
|
||||
node-webpmux: "npm:3.2.1"
|
||||
puppeteer: "npm:24.38.0"
|
||||
unzipper: "npm:0.12.3"
|
||||
dependenciesMeta:
|
||||
archiver:
|
||||
optional: true
|
||||
@@ -14222,7 +14165,7 @@ __metadata:
|
||||
optional: true
|
||||
unzipper:
|
||||
optional: true
|
||||
checksum: 10/705f16bd7c70afae7d8a76fbfc640fbb2be7d8d99695a694f0953507716c65cbbd44f68dcf5298bc5d61fa8cc55dd5d2ec9bc3d1cfc51281a35723b936c0994c
|
||||
checksum: 10/c9493ee9f0034f2fcd5234575f140dadb1402dbfa143af88efc8c489bc7b43d0375e83c5fa01ad507f0d3ed5982ad8504f6802aa7f95c0af8293a051a2e99ba6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -14539,14 +14482,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zip-stream@npm:^4.1.0":
|
||||
version: 4.1.1
|
||||
resolution: "zip-stream@npm:4.1.1"
|
||||
"zip-stream@npm:^6.0.1":
|
||||
version: 6.0.1
|
||||
resolution: "zip-stream@npm:6.0.1"
|
||||
dependencies:
|
||||
archiver-utils: "npm:^3.0.4"
|
||||
compress-commons: "npm:^4.1.2"
|
||||
readable-stream: "npm:^3.6.0"
|
||||
checksum: 10/33bd5ee7017656c2ad728b5d4ba510e15bd65ce1ec180c5bbdc7a5f063256353ec482e6a2bc74de7515219d8494147924b9aae16e63fdaaf37cdf7d1ee8df125
|
||||
archiver-utils: "npm:^5.0.0"
|
||||
compress-commons: "npm:^6.0.2"
|
||||
readable-stream: "npm:^4.0.0"
|
||||
checksum: 10/aa5abd6a89590eadeba040afbc375f53337f12637e5e98330012a12d9886cde7a3ccc28bd91aafab50576035bbb1de39a9a316eecf2411c8b9009c9f94f0db27
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in new issue
Block a user