Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
768 changes: 286 additions & 482 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
"amqplib": "^0.10.5",
"audio-decode": "^2.2.3",
"axios": "^1.7.9",
"baileys": "7.0.0-rc.9",
"baileys": "^7.0.0-rc13",
"class-validator": "^0.14.1",
"compression": "^1.7.5",
"cors": "^2.8.5",
Expand All @@ -87,10 +87,10 @@
"eventemitter2": "^6.4.9",
"express": "^4.21.2",
"express-async-errors": "^3.1.1",
"fetch-socks": "^1.3.2",
"fluent-ffmpeg": "^2.1.3",
"form-data": "^4.0.1",
"https-proxy-agent": "^7.0.6",
"fetch-socks": "^1.3.2",
"i18next": "^23.7.19",
"jimp": "^1.6.0",
"json-schema": "^0.4.0",
Expand Down
19 changes: 19 additions & 0 deletions src/api/controllers/group.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import {
GroupDescriptionDto,
GroupInvite,
GroupJid,
GroupJoinApprovalModeDto,
GroupMemberAddModeDto,
GroupPictureDto,
GroupSendInvite,
GroupSubjectDto,
GroupToggleEphemeralDto,
GroupUpdateParticipantDto,
GroupUpdateParticipantRequestDto,
GroupUpdateSettingDto,
} from '@api/dto/group.dto';
import { InstanceDto } from '@api/dto/instance.dto';
Expand Down Expand Up @@ -78,6 +81,22 @@ export class GroupController {
return await this.waMonitor.waInstances[instance.instanceName].toggleEphemeral(update);
}

public async updateMemberAddMode(instance: InstanceDto, update: GroupMemberAddModeDto) {
return await this.waMonitor.waInstances[instance.instanceName].updateMemberAddMode(update);
}

public async updateJoinApprovalMode(instance: InstanceDto, update: GroupJoinApprovalModeDto) {
return await this.waMonitor.waInstances[instance.instanceName].updateJoinApprovalMode(update);
}

public async findParticipantRequests(instance: InstanceDto, groupJid: GroupJid) {
return await this.waMonitor.waInstances[instance.instanceName].findParticipantRequests(groupJid);
}

public async updateParticipantRequests(instance: InstanceDto, update: GroupUpdateParticipantRequestDto) {
return await this.waMonitor.waInstances[instance.instanceName].updateParticipantRequests(update);
}

public async leaveGroup(instance: InstanceDto, groupJid: GroupJid) {
return await this.waMonitor.waInstances[instance.instanceName].leaveGroup(groupJid);
}
Expand Down
5 changes: 5 additions & 0 deletions src/api/controllers/sendMessage.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { InstanceDto } from '@api/dto/instance.dto';
import {
ForwardMessageDto,
SendAudioDto,
SendButtonsDto,
SendContactDto,
Expand Down Expand Up @@ -39,6 +40,10 @@ export class SendMessageController {
return await this.waMonitor.waInstances[instanceName].textMessage(data);
}

public async forwardMessage({ instanceName }: InstanceDto, data: ForwardMessageDto) {
return await this.waMonitor.waInstances[instanceName].forwardMessage(data);
}

public async sendMedia({ instanceName }: InstanceDto, data: SendMediaDto, file?: any) {
if (isBase64(data?.media) && !data?.fileName && data?.mediatype === 'document') {
throw new BadRequestException('For base64 the file name must be informed.');
Expand Down
13 changes: 13 additions & 0 deletions src/api/dto/group.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,16 @@ export class GroupUpdateSettingDto extends GroupJid {
export class GroupToggleEphemeralDto extends GroupJid {
expiration: 0 | 86400 | 604800 | 7776000;
}

export class GroupMemberAddModeDto extends GroupJid {
mode: 'admin_add' | 'all_member_add';
}

export class GroupJoinApprovalModeDto extends GroupJid {
mode: 'on' | 'off';
}

export class GroupUpdateParticipantRequestDto extends GroupJid {
action: 'approve' | 'reject';
participants: string[];
}
4 changes: 4 additions & 0 deletions src/api/dto/sendMessage.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export class Metadata {
export class SendTextDto extends Metadata {
text: string;
}
export class ForwardMessageDto {
number: string;
messageId: string;
}
export class SendPresence extends Metadata {
text: string;
}
Expand Down
95 changes: 80 additions & 15 deletions src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,22 @@ import {
GroupDescriptionDto,
GroupInvite,
GroupJid,
GroupJoinApprovalModeDto,
GroupMemberAddModeDto,
GroupPictureDto,
GroupSendInvite,
GroupSubjectDto,
GroupToggleEphemeralDto,
GroupUpdateParticipantDto,
GroupUpdateParticipantRequestDto,
GroupUpdateSettingDto,
} from '@api/dto/group.dto';
import { InstanceDto, SetPresenceDto } from '@api/dto/instance.dto';
import { HandleLabelDto, LabelDto } from '@api/dto/label.dto';
import {
Button,
ContactMessage,
ForwardMessageDto,
KeyType,
MediaMessage,
Options,
Expand Down Expand Up @@ -224,6 +228,12 @@ async function getVideoDuration(input: Buffer | string | Readable): Promise<numb
return Math.round(parseFloat(duration));
}

// Throttle global de groupMetadata: serializa e espaca as buscas pra nao
// estourar o rate-limit (429) do WhatsApp em contas com centenas de grupos.
let __groupMetaChain: Promise<unknown> = Promise.resolve();
let __groupMetaLastAt = 0;
const __GROUP_META_MIN_INTERVAL_MS = 2000;

export class BaileysStartupService extends ChannelStartupService {
private messageProcessor = new BaileysMessageProcessor();

Expand Down Expand Up @@ -553,6 +563,20 @@ export class BaileysStartupService extends ChannelStartupService {
}
}

public async forwardMessage(data: ForwardMessageDto) {
try {
const fullMsg = (await this.getMessage({ id: data.messageId }, true)) as unknown as WAMessage;
if (!fullMsg?.message) {
throw new BadRequestException('Message not found');
}
const number = data.number.replace(/\D/g, '');
const jid = data.number.includes('@') ? data.number : `${number}@s.whatsapp.net`;
return await this.client.sendMessage(jid, { forward: fullMsg });
} catch (error) {
throw new BadRequestException('Error forwarding message', error.toString());
}
}

private async defineAuthState() {
const db = this.configService.get<Database>('DATABASE');
const cache = this.configService.get<CacheConf>('CACHE');
Expand Down Expand Up @@ -4285,21 +4309,24 @@ export class BaileysStartupService extends ChannelStartupService {

// Group
private async updateGroupMetadataCache(groupJid: string) {
try {
const meta = await this.client.groupMetadata(groupJid);

const cacheConf = this.configService.get<CacheConf>('CACHE');

if ((cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') || cacheConf?.LOCAL?.ENABLED) {
this.logger.verbose(`Updating cache for group: ${groupJid}`);
await groupMetadataCache.set(groupJid, { timestamp: Date.now(), data: meta });
const task = __groupMetaChain.then(async () => {
const wait = __GROUP_META_MIN_INTERVAL_MS - (Date.now() - __groupMetaLastAt);
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
__groupMetaLastAt = Date.now();
try {
const meta = await this.client.groupMetadata(groupJid);
const cacheConf = this.configService.get<CacheConf>('CACHE');
if ((cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') || cacheConf?.LOCAL?.ENABLED) {
await groupMetadataCache.set(groupJid, { timestamp: Date.now(), data: meta });
}
return meta;
} catch (error) {
this.logger.error(error);
return null;
}

return meta;
} catch (error) {
this.logger.error(error);
return null;
}
});
__groupMetaChain = task.then(() => undefined, () => undefined);
return task;
}

private getGroupMetadataCache = async (groupJid: string) => {
Expand Down Expand Up @@ -4450,7 +4477,8 @@ export class BaileysStartupService extends ChannelStartupService {

let groups = [];
for (const group of fetch) {
const picture = await this.profilePicture(group.id);
const wantDetails = getParticipants.getParticipants == 'true';
const picture = wantDetails ? await this.profilePicture(group.id) : null;

const result = {
id: group.id,
Expand Down Expand Up @@ -4597,6 +4625,43 @@ export class BaileysStartupService extends ChannelStartupService {
}
}

public async updateMemberAddMode(update: GroupMemberAddModeDto) {
try {
await this.client.groupMemberAddMode(update.groupJid, update.mode);
return { success: true };
} catch (error) {
throw new BadRequestException('Error updating member add mode', error.toString());
}
}

public async updateJoinApprovalMode(update: GroupJoinApprovalModeDto) {
try {
await this.client.groupJoinApprovalMode(update.groupJid, update.mode);
return { success: true };
} catch (error) {
throw new BadRequestException('Error updating join approval mode', error.toString());
}
}

public async findParticipantRequests(id: GroupJid) {
try {
const requests = await this.client.groupRequestParticipantsList(id.groupJid);
return { requests: requests || [] };
} catch (error) {
throw new BadRequestException('Error fetching participant requests', error.toString());
}
}

public async updateParticipantRequests(update: GroupUpdateParticipantRequestDto) {
try {
const participants = update.participants.map((p) => (p.includes('@') ? p : `${p}@s.whatsapp.net`));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  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.

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());
}
}

public async leaveGroup(id: GroupJid) {
try {
await this.client.groupLeave(id.groupJid);
Expand Down
46 changes: 46 additions & 0 deletions src/api/routes/group.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import {
GroupDescriptionDto,
GroupInvite,
GroupJid,
GroupJoinApprovalModeDto,
GroupMemberAddModeDto,
GroupPictureDto,
GroupSendInvite,
GroupSubjectDto,
GroupToggleEphemeralDto,
GroupUpdateParticipantDto,
GroupUpdateParticipantRequestDto,
GroupUpdateSettingDto,
} from '@api/dto/group.dto';
import { groupController } from '@api/server.module';
Expand All @@ -21,10 +24,13 @@ import {
groupInviteSchema,
groupJidSchema,
groupSendInviteSchema,
joinApprovalModeSchema,
memberAddModeSchema,
toggleEphemeralSchema,
updateGroupDescriptionSchema,
updateGroupPictureSchema,
updateGroupSubjectSchema,
updateParticipantRequestSchema,
updateParticipantsSchema,
updateSettingsSchema,
} from '@validate/validate.schema';
Expand Down Expand Up @@ -186,6 +192,46 @@ export class GroupRouter extends RouterBroker {

res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('memberAddMode'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupMemberAddModeDto>({
request: req,
schema: memberAddModeSchema,
ClassRef: GroupMemberAddModeDto,
execute: (instance, data) => groupController.updateMemberAddMode(instance, data),
});

res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('joinApprovalMode'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupJoinApprovalModeDto>({
request: req,
schema: joinApprovalModeSchema,
ClassRef: GroupJoinApprovalModeDto,
execute: (instance, data) => groupController.updateJoinApprovalMode(instance, data),
});

res.status(HttpStatus.CREATED).json(response);
})
.get(this.routerPath('participantRequests'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupJid>({
request: req,
schema: groupJidSchema,
ClassRef: GroupJid,
execute: (instance, data) => groupController.findParticipantRequests(instance, data),
});

res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('updateParticipantRequests'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupUpdateParticipantRequestDto>({
request: req,
schema: updateParticipantRequestSchema,
ClassRef: GroupUpdateParticipantRequestDto,
execute: (instance, data) => groupController.updateParticipantRequests(instance, data),
});

res.status(HttpStatus.CREATED).json(response);
})
.delete(this.routerPath('leaveGroup'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupJid>({
request: req,
Expand Down
12 changes: 12 additions & 0 deletions src/api/routes/sendMessage.router.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import {
ForwardMessageDto,
SendAudioDto,
SendButtonsDto,
SendContactDto,
Expand All @@ -19,6 +20,7 @@ import {
audioMessageSchema,
buttonsMessageSchema,
contactMessageSchema,
forwardMessageSchema,
listMessageSchema,
locationMessageSchema,
mediaMessageSchema,
Expand Down Expand Up @@ -61,6 +63,16 @@ export class MessageRouter extends RouterBroker {

return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('forwardMessage'), ...guards, async (req, res) => {
const response = await this.dataValidate<ForwardMessageDto>({
request: req,
schema: forwardMessageSchema,
ClassRef: ForwardMessageDto,
execute: (instance, data) => sendMessageController.forwardMessage(instance, data),
});

return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendMedia'), ...guards, upload.single('file'), async (req, res) => {
const bodyData = req.body;

Expand Down
Loading