Repository navigation
feat(group): member-add-mode, join-approval-mode, participant requests + message forward - #2595
Conversation
…s + message forward
Exposes Baileys group/message capabilities that already exist in the underlying
library but had no REST endpoints:
- POST /group/memberAddMode -> groupMemberAddMode (admin_add | all_member_add)
- POST /group/joinApprovalMode -> groupJoinApprovalMode (on | off)
- GET /group/participantRequests -> groupRequestParticipantsList (pending join requests)
- POST /group/updateParticipantRequests -> groupRequestParticipantsUpdate (approve | reject)
- POST /message/forwardMessage -> sendMessage(jid, { forward }) (keeps isForwarded)
Each follows the existing pattern (DTO + JSONSchema + router + controller +
baileys service). No new dependencies — baileys 7.0.0-rc.9 already ships these.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewer's GuideAdds REST endpoints and DTO/schema/controller/service plumbing to expose new WhatsApp group admin capabilities (member-add restrictions, join approval, participant request management) and a message-forwarding endpoint that reuses stored WAMessages and Baileys forwarding, following existing validation/router patterns. Sequence diagram for forwarding a stored WhatsApp messagesequenceDiagram
actor Client
participant MessageRouter
participant SendMessageController
participant BaileysStartupService
participant BaileysClient
Client->>MessageRouter: POST /message/forwardMessage
MessageRouter->>MessageRouter: dataValidate ForwardMessageDto
MessageRouter->>SendMessageController: forwardMessage(instance, data)
SendMessageController->>BaileysStartupService: forwardMessage(data)
BaileysStartupService->>BaileysStartupService: getMessage({ id: messageId }, true)
BaileysStartupService->>BaileysClient: sendMessage(jid, { forward: fullMsg })
BaileysClient-->>BaileysStartupService: sendMessage result
BaileysStartupService-->>SendMessageController: forward result
SendMessageController-->>MessageRouter: response
MessageRouter-->>Client: 201 Created
Sequence diagram for group participant request management endpointssequenceDiagram
actor Admin
participant GroupRouter
participant GroupController
participant BaileysStartupService
participant BaileysClient
rect rgb(235, 245, 255)
Admin->>GroupRouter: GET /group/participantRequests
GroupRouter->>GroupRouter: groupValidate GroupJid
GroupRouter->>GroupController: findParticipantRequests(instance, groupJid)
GroupController->>BaileysStartupService: findParticipantRequests(groupJid)
BaileysStartupService->>BaileysClient: groupRequestParticipantsList(groupJid)
BaileysClient-->>BaileysStartupService: requests
BaileysStartupService-->>GroupController: { requests }
GroupController-->>GroupRouter: { requests }
GroupRouter-->>Admin: 200 OK
end
rect rgb(235, 255, 235)
Admin->>GroupRouter: POST /group/updateParticipantRequests
GroupRouter->>GroupRouter: groupValidate GroupUpdateParticipantRequestDto
GroupRouter->>GroupController: updateParticipantRequests(instance, update)
GroupController->>BaileysStartupService: updateParticipantRequests(update)
BaileysStartupService->>BaileysClient: groupRequestParticipantsUpdate(groupJid, participants, action)
BaileysClient-->>BaileysStartupService: update result
BaileysStartupService-->>GroupController: { updateParticipantRequests: result }
GroupController-->>GroupRouter: response
GroupRouter-->>Admin: 201 Created
end
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- In
forwardMessage, the innerthrow new BadRequestException('Message not found')is immediately caught and wrapped as a generic'Error forwarding message', so callers lose the specific error; consider either not catching that case or rethrowing it unchanged whenfullMsg?.messageis missing. - The logic to normalize phone numbers/JIDs (e.g.,
number.replace(/\D/g, '')and appending@s.whatsapp.netinforwardMessageandupdateParticipantRequests) appears ad hoc; consider extracting and reusing a shared helper to keep JID handling consistent across endpoints. - In
updateParticipantRequestSchema,participantsis required but not included in theisNotEmptycall, so empty strings would pass validation; it may be worth addingparticipantstoisNotEmptyor tightening the item schema if you want to reject blank participant entries.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `forwardMessage`, the inner `throw new BadRequestException('Message not found')` is immediately caught and wrapped as a generic `'Error forwarding message'`, so callers lose the specific error; consider either not catching that case or rethrowing it unchanged when `fullMsg?.message` is missing.
- The logic to normalize phone numbers/JIDs (e.g., `number.replace(/\D/g, '')` and appending `@s.whatsapp.net` in `forwardMessage` and `updateParticipantRequests`) appears ad hoc; consider extracting and reusing a shared helper to keep JID handling consistent across endpoints.
- In `updateParticipantRequestSchema`, `participants` is required but not included in the `isNotEmpty` call, so empty strings would pass validation; it may be worth adding `participants` to `isNotEmpty` or tightening the item schema if you want to reject blank participant entries.
## Individual Comments
### Comment 1
<location path="src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts" line_range="4647" />
<code_context>
+
+ public async updateParticipantRequests(update: GroupUpdateParticipantRequestDto) {
+ try {
+ const participants = update.participants.map((p) => (p.includes('@') ? p : `${p}@s.whatsapp.net`));
+ const result = await this.client.groupRequestParticipantsUpdate(update.groupJid, participants, update.action);
+ return { updateParticipantRequests: result };
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Participant JID normalization is inconsistent with number normalization elsewhere and may accept malformed identifiers.
Participants are treated as JIDs whenever they contain `@`, and otherwise `@s.whatsapp.net` is appended, but there’s no stripping/validation of the local part. If callers pass phone numbers with spaces, dashes, or other formatting, this will produce invalid JIDs. Please either reuse the same normalization helper used in `forwardMessage` (or similar) or enforce that inputs are already-normalized JIDs or strictly numeric IDs before appending the domain.
Suggested implementation:
```typescript
public async updateParticipantRequests(update: GroupUpdateParticipantRequestDto) {
try {
const normalizeParticipant = (raw: string): string => {
const value = raw.trim();
if (!value) {
throw new BadRequestException('Participant identifier cannot be empty');
}
// If it's already a JID, return as-is (after trimming)
if (value.includes('@')) {
return value;
}
// Treat as phone-like input: strip non-digits and validate
const digitsOnly = value.replace(/\D/g, '');
if (!digitsOnly) {
throw new BadRequestException(
`Invalid participant identifier "${raw}". Expected a JID or numeric phone number.`,
);
}
return `${digitsOnly}@s.whatsapp.net`;
};
const participants = update.participants.map(normalizeParticipant);
const result = await this.client.groupRequestParticipantsUpdate(update.groupJid, participants, update.action);
return { updateParticipantRequests: result };
} catch (error) {
throw new BadRequestException('Error updating participant requests', error.toString());
}
}
```
If your codebase already has a shared normalization helper (e.g. used in `forwardMessage`), consider:
1. Replacing the in-method `normalizeParticipant` function with a call to that shared helper for consistency.
2. If that helper returns bare numbers instead of full JIDs, adjust the logic to append `@s.whatsapp.net` only when necessary, keeping the validation/stripping behavior consistent.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
|
||
| public async updateParticipantRequests(update: GroupUpdateParticipantRequestDto) { | ||
| try { | ||
| const participants = update.participants.map((p) => (p.includes('@') ? p : `${p}@s.whatsapp.net`)); |
There was a problem hiding this comment.
suggestion (bug_risk): Participant JID normalization is inconsistent with number normalization elsewhere and may accept malformed identifiers.
Participants are treated as JIDs whenever they contain @, and otherwise @s.whatsapp.net is appended, but there’s no stripping/validation of the local part. If callers pass phone numbers with spaces, dashes, or other formatting, this will produce invalid JIDs. Please either reuse the same normalization helper used in forwardMessage (or similar) or enforce that inputs are already-normalized JIDs or strictly numeric IDs before appending the domain.
Suggested implementation:
public async updateParticipantRequests(update: GroupUpdateParticipantRequestDto) {
try {
const normalizeParticipant = (raw: string): string => {
const value = raw.trim();
if (!value) {
throw new BadRequestException('Participant identifier cannot be empty');
}
// If it's already a JID, return as-is (after trimming)
if (value.includes('@')) {
return value;
}
// Treat as phone-like input: strip non-digits and validate
const digitsOnly = value.replace(/\D/g, '');
if (!digitsOnly) {
throw new BadRequestException(
`Invalid participant identifier "${raw}". Expected a JID or numeric phone number.`,
);
}
return `${digitsOnly}@s.whatsapp.net`;
};
const participants = update.participants.map(normalizeParticipant);
const result = await this.client.groupRequestParticipantsUpdate(update.groupJid, participants, update.action);
return { updateParticipantRequests: result };
} catch (error) {
throw new BadRequestException('Error updating participant requests', error.toString());
}
}If your codebase already has a shared normalization helper (e.g. used in forwardMessage), consider:
- Replacing the in-method
normalizeParticipantfunction with a call to that shared helper for consistency. - If that helper returns bare numbers instead of full JIDs, adjust the logic to append
@s.whatsapp.netonly when necessary, keeping the validation/stripping behavior consistent.
|
🔴 👎 A feature de grupo (member-add-mode, join-approval, participant requests) parece útil e aditiva, mas o PR aponta para |
… de grupos) + baileys rc13
… profilePicture — listagem sem rajada)
| "node_modules/libsignal": { | ||
| "name": "@whiskeysockets/libsignal-node", | ||
| "version": "2.0.1", | ||
| "resolved": "git+ssh://git@github.com/whiskeysockets/libsignal-node.git#1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67", | ||
| "version": "6.0.0", | ||
| "resolved": "https://registry.npmjs.org/libsignal/-/libsignal-6.0.0.tgz", | ||
| "integrity": "sha512-d/5V3YFtDljbFMufz4ncyUYGYhJl+vzAe+c2EFFBQ6bz1h8Q3IOMEGXYMzlibU60I+e8GagMMpji18iez3P1hA==", | ||
| "license": "GPL-3.0", | ||
| "dependencies": { | ||
| "curve25519-js": "^0.0.4", | ||
| "protobufjs": "6.8.8" | ||
| } | ||
| }, | ||
| "node_modules/libsignal/node_modules/@types/node": { | ||
| "version": "10.17.60", | ||
| "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", | ||
| "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", | ||
| "license": "MIT" | ||
| }, | ||
| "node_modules/libsignal/node_modules/long": { | ||
| "version": "4.0.0", | ||
| "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", | ||
| "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", | ||
| "license": "Apache-2.0" | ||
| }, | ||
| "node_modules/libsignal/node_modules/protobufjs": { | ||
| "version": "6.8.8", | ||
| "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", | ||
| "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", | ||
| "hasInstallScript": true, | ||
| "license": "BSD-3-Clause", | ||
| "dependencies": { | ||
| "@protobufjs/aspromise": "^1.1.2", | ||
| "@protobufjs/base64": "^1.1.2", | ||
| "@protobufjs/codegen": "^2.0.4", | ||
| "@protobufjs/eventemitter": "^1.1.0", | ||
| "@protobufjs/fetch": "^1.1.0", | ||
| "@protobufjs/float": "^1.0.2", | ||
| "@protobufjs/inquire": "^1.1.0", | ||
| "@protobufjs/path": "^1.1.2", | ||
| "@protobufjs/pool": "^1.1.0", | ||
| "@protobufjs/utf8": "^1.1.0", | ||
| "@types/long": "^4.0.0", | ||
| "@types/node": "^10.1.0", | ||
| "long": "^4.0.0" | ||
| }, | ||
| "bin": { | ||
| "pbjs": "bin/pbjs", | ||
| "pbts": "bin/pbts" | ||
| "protobufjs": "^7.5.5" | ||
| } | ||
| }, |
There was a problem hiding this comment.
security (license/libsignal): GPL-3.0-only: Open-source license can require releasing the entire application source
This GPL-3.0-only open-source license can impose strong copyleft or non-commercial obligations that may require releasing your full application source code or restrict commercial use, depending on how the code is used or distributed
Source: trivy
|
need to appoint to develop (not to main) @alpesdigital-adm |
Co-authored-by: root <root@srv1512423.hstgr.cloud>
chats.update arrives keyed by LID on accounts migrated to LID addressing, while inbound messages are already normalized to the phone JID. Webhook consumers could not match the read state to the conversation, so reading on the phone never cleared unread counts downstream. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Webhook consumers only see the phone JID, but LID-addressed chats are indexed by LID on the primary device. Receipts and app-state patches sent to the phone JID target a chat the phone does not know. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The auth-state adapters rebuilt AppStateSyncKeyData with create(), which keeps keyData as the stored base64 string. Baileys then derived the app-state keys from the 44-char text instead of the 32 key bytes: every incoming patch failed to decrypt and was skipped silently (MAC checks are off by default), and every outgoing chatModify patch was encrypted with the wrong keys, which the server answers with 401 device_removed. fromObject decodes base64 into bytes, as Baileys useMultiFileAuthState. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(CVE pending, evolution-foundation#2435) (evolution-foundation#2549) * security: prevent cross-instance auth bypass via query/body override (evolution-foundation#2435) The auth guard in src/api/guards/auth.guard.ts validates instance ownership using req.params.instanceName. The abstract router then merged req.query (and, on /instance/create, req.body) into the instance object via Object.assign — which silently overwrote the already-authenticated instanceName. An attacker with one valid token could send: GET /chat/findMessages/MY_INSTANCE?instanceName=VICTIM_INSTANCE Auth passed for MY_INSTANCE, but dataValidate() then replaced the instance with VICTIM_INSTANCE before execute() ran — giving the caller full access to read/send messages, modify settings, and delete other tenants' instances. CWE-639: Authorization Bypass Through User-Controlled Key Fix: introduce sanitizeUntrustedInput() that strips PROTECTED_INSTANCE_FIELDS (instanceName, instanceId) from any untrusted source before merging into the instance object. Logs a warning on attempts so abuse is auditable. Closes evolution-foundation#2435 Reported by @lighthousekeeper1212 via static analysis. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(lint): remove extra blank line introduced by recent merge Pre-push lint flagged whatsapp.baileys.service.ts:531 (double blank line after the stream:error 515 fix merged via evolution-foundation#2509). Trivial prettier fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> (cherry picked from commit 7a55a2b)
…m:error 515 (evolution-foundation#2509) * fix(baileys): prevent healthy instances from being killed after stream:error 515 When WhatsApp sends stream:error code=515 (Connection Replaced), Baileys handles the reconnect correctly and fires connection.update with state='open'. However, WhatsApp then sends a 401 (loggedOut) to clean up the old session slot, which Evolution API incorrectly treated as a real logout, killing the newly-connected healthy instance. The fix tracks when a stream:error 515 node arrives via the CB:stream:error WebSocket event. If a loggedOut (401) close event fires within 30 seconds of a 515, it is treated as a transient reconnect rather than a real logout. Fixes evolution-foundation#2498 * fix(baileys): name the 515 reconnect grace + tighten stream-error types Addresses sourcery-ai review feedback on the previous commit: - Extract the 30 000ms reconnect grace window into a named class constant STREAM_515_RECONNECT_GRACE_MS so future tuning is self-documenting rather than a literal scattered through the close handler. - Extract the magic '515' string into STREAM_ERROR_CODE_RECONNECT. - Replace the loose 'node: any' on the 'CB:stream:error' handler with a minimal structural type ({ attrs?: { code?: string | number } }) so the payload shape is documented and type-checked. - Compare the code via String(...) so a numeric 515 from the underlying socket library still triggers the grace window — the original literal '515' check would have silently broken on a type change. * fix(baileys): satisfy prettier — collapse 515 reconnect guard onto one line Check Code Quality lint failed on prettier/prettier (the recentStream515 expression fits within the project's 120-col printWidth on a single line, while shouldReconnect still needs to break across two). Co-authored-by: Octopus <liyuan851277048@icloud.com> --------- Co-authored-by: octo-patch <octo-patch@github.com> Co-authored-by: octo-patch <octo-patch@users.noreply.github.com> (cherry picked from commit 1d38d4f)
sanitizeUntrustedInput on /instance/create removed instanceName from the body, which is the only source of the new instance name. Create has no authenticated instance in the path, so there is nothing to override there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nests the tc token under the picture query in profilePictureUrl, so profile pictures of privacy-restricted contacts resolve. The Android browser mode is opt-in by browser name and stays off here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
History sync after a fresh pairing (and some live messages) deliver LID-only keys. Without the phone JID, webhook consumers cannot identify the sender and drop the message. Resolve it with getPNForLID, live and in messaging-history.set; unknown mappings leave the key untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
History messages without a known contact got pushName = JID user part (LID digits included), which CRMs then stored as the contact name. Look the contact up by the phone JID as well and otherwise leave it empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What
Adds REST endpoints for group/message features that baileys (the underlying lib) already implements but Evolution doesn't expose yet:
POST /group/memberAddModegroupMemberAddMode— restrict who adds members (admin_add/all_member_add)POST /group/joinApprovalModegroupJoinApprovalMode— turn membership approval on/off (on/off)GET /group/participantRequestsgroupRequestParticipantsList— list pending join requestsPOST /group/updateParticipantRequestsgroupRequestParticipantsUpdate— approve/reject requestsPOST /message/forwardMessagesendMessage(jid, { forward })— forward a stored message (keeps the forwarded tag)Why
Common group-admin needs (membership approval, locking member-add) and message forwarding. All supported by
baileys(7.0.0-rc.9in this repo) but with no HTTP surface until now.How
Each endpoint follows the existing
updateSetting/toggleEphemeralpattern: DTO + JSONSchema + router + controller + baileys service call.forwardMessagereuses the privategetMessage(key, full)to load the storedWAMessageand passes it tosendMessage(jid, { forward }).No new dependencies.
tsc --noEmit+tsupbuild pass; pre-commit lint clean.🤖 Generated with Claude Code
Summary by Sourcery
Expand WhatsApp group and message APIs while strengthening instance safety, authentication state handling, reconnection, and chat read-state synchronization.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Tests: