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
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
55 changes: 55 additions & 0 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 @@ -553,6 +557,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 @@ -4597,6 +4615,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
39 changes: 39 additions & 0 deletions src/validate/group.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,45 @@ export const toggleEphemeralSchema: JSONSchema7 = {
...isNotEmpty('groupJid', 'expiration'),
};

export const memberAddModeSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
groupJid: { type: 'string' },
mode: { type: 'string', enum: ['admin_add', 'all_member_add'] },
},
required: ['groupJid', 'mode'],
...isNotEmpty('groupJid', 'mode'),
};

export const joinApprovalModeSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
groupJid: { type: 'string' },
mode: { type: 'string', enum: ['on', 'off'] },
},
required: ['groupJid', 'mode'],
...isNotEmpty('groupJid', 'mode'),
};

export const updateParticipantRequestSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
groupJid: { type: 'string' },
action: { type: 'string', enum: ['approve', 'reject'] },
participants: {
type: 'array',
minItems: 1,
uniqueItems: true,
items: { type: 'string' },
},
},
required: ['groupJid', 'action', 'participants'],
...isNotEmpty('groupJid', 'action'),
};

export const updateGroupPictureSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
Expand Down
11 changes: 11 additions & 0 deletions src/validate/message.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ export const offerCallSchema: JSONSchema7 = {
required: ['number', 'callDuration'],
};

export const forwardMessageSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
number: { ...numberDefinition },
messageId: { type: 'string' },
},
required: ['number', 'messageId'],
...isNotEmpty('number', 'messageId'),
};

export const textMessageSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
Expand Down