Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
11 changes: 11 additions & 0 deletions src/api/controllers/contact.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { SaveContactDto } from '@api/dto/contact.dto';
import { InstanceDto } from '@api/dto/instance.dto';
import { WAMonitoringService } from '@api/services/monitor.service';

export class ContactController {
constructor(private readonly waMonitor: WAMonitoringService) {}

public async saveContact({ instanceName }: InstanceDto, data: SaveContactDto) {
return await this.waMonitor.waInstances[instanceName].saveContact(data);
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The saveContact method is missing from the evolution and meta provider implementations. Looking at the pattern in evolution.channel.service.ts (lines 783-794) and whatsapp.business.service.ts (lines 1666-1680), methods that are not available in a provider should have stub implementations that throw a BadRequestException with a descriptive message. Without these stub methods, calling this endpoint with an evolution or meta instance will result in a runtime error ('saveContact is not a function') instead of a proper error response. Add stub implementations in both evolution.channel.service.ts and whatsapp.business.service.ts.

Suggested change
export class ContactController {
constructor(private readonly waMonitor: WAMonitoringService) {}
public async saveContact({ instanceName }: InstanceDto, data: SaveContactDto) {
return await this.waMonitor.waInstances[instanceName].saveContact(data);
import { BadRequestException } from '@nestjs/common';
export class ContactController {
constructor(private readonly waMonitor: WAMonitoringService) {}
public async saveContact({ instanceName }: InstanceDto, data: SaveContactDto) {
const instance = this.waMonitor.waInstances[instanceName];
if (!instance || typeof instance.saveContact !== 'function') {
throw new BadRequestException(
`saveContact is not supported for the provider used by instance "${instanceName}"`,
);
}
return await instance.saveContact(data);

Copilot uses AI. Check for mistakes.
}
}
5 changes: 5 additions & 0 deletions src/api/dto/contact.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class SaveContactDto {
number: string;
name: string;
saveOnDevice?: boolean;
}
24 changes: 24 additions & 0 deletions src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getCollectionsDto } from '@api/dto/business.dto';

Check failure on line 1 in src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Run autofix to sort these imports!
import { OfferCallDto } from '@api/dto/call.dto';
import {
ArchiveChatDto,
Expand Down Expand Up @@ -154,6 +154,7 @@

import { BaileysMessageProcessor } from './baileysMessage.processor';
import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys';
import { SaveContactDto } from '@api/dto/contact.dto';

export interface ExtendedIMessageKey extends proto.IMessageKey {
remoteJidAlt?: string;
Expand Down Expand Up @@ -3767,6 +3768,29 @@
});
}
}

Check failure on line 3771 in src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

View workflow job for this annotation

GitHub Actions / check-lint-and-build

Delete `··`
public async saveContact(data: SaveContactDto) {
try {
const jid = createJid(data.number);
await this.client.chatModify(
{
contact: {
fullName: data.name || 'Unknown',
firstName: (data.name || 'Unknown').split(' ')[0],
saveOnPrimaryAddressbook: data.saveOnDevice ?? true,
},
},
jid,
);

return { saved: true, number: data.number, name: data.name };
} catch (error) {
throw new InternalServerErrorException({
saved: false,
message: ['An error occurred while saving the contact.', error.toString()],
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The error message uses 'An error occurred while saving the contact.' but looking at similar methods in the codebase (archiveChat at line 3739, markChatUnread at line 3767), they include more detailed guidance like 'Open a calling.' Consider adding more context to help users understand what action to take when this error occurs, following the pattern established in the codebase.

Suggested change
message: ['An error occurred while saving the contact.', error.toString()],
message: ['An error occurred while saving the contact.', 'Open a calling.', error.toString()],

Copilot uses AI. Check for mistakes.
});
}
}

public async deleteMessage(del: DeleteMessage) {
try {
Expand Down
29 changes: 29 additions & 0 deletions src/api/routes/contact.router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { SaveContactDto } from '@api/dto/contact.dto';
import { contactController } from '@api/server.module';
import { saveContactSchema } from '@validate/contact.schema';
import { RequestHandler, Router } from 'express';

import { HttpStatus } from './index.router';

export class ContactRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router.post(this.routerPath('save'), ...guards, async (req, res) => {
try {
const response = await this.dataValidate<SaveContactDto>({
request: req,
schema: saveContactSchema,
ClassRef: SaveContactDto,
execute: (instance, data) => contactController.saveContact(instance, data),
});

return res.status(HttpStatus.OK).json(response);
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The HTTP status code should be HttpStatus.CREATED instead of HttpStatus.OK for a create/save operation. This is consistent with similar operations in the codebase: CallRouter uses CREATED for offerCall, ChatRouter uses CREATED for archiveChat/markChatUnread, and GroupRouter uses CREATED for create operations. Since this endpoint is creating/saving a new contact, it should return a 201 CREATED status code.

Copilot uses AI. Check for mistakes.
} catch (error) {
return res.status(HttpStatus.BAD_REQUEST).json(error);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Differentiate between validation errors and server errors instead of always returning 400.

The catch block currently turns every error into HttpStatus.BAD_REQUEST, including actual server failures from calls like waInstances[instanceName].saveContact. This misclassifies errors for clients. Instead, only map known validation errors to 400 and default everything else to 500 (or use error.status || HttpStatus.INTERNAL_SERVER_ERROR when that metadata exists).

Suggested change
} catch (error) {
return res.status(HttpStatus.BAD_REQUEST).json(error);
}
} catch (error) {
const err = error as any;
const isValidationError =
err?.name === 'ValidationError' ||
err?.isJoi === true ||
err?.status === HttpStatus.BAD_REQUEST;
const status = isValidationError
? HttpStatus.BAD_REQUEST
: err?.status || HttpStatus.INTERNAL_SERVER_ERROR;
return res.status(status).json(err);
}

Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The try-catch block here is inconsistent with the codebase conventions. Most routers in the codebase (GroupRouter, LabelRouter, and most routes in ChatRouter) do not wrap dataValidate calls in try-catch blocks, relying instead on the 'express-async-errors' package imported in the abstract router to handle async errors automatically. The try-catch pattern is only used in a few specific cases in BusinessRouter where custom error handling is needed. For consistency, remove the try-catch block and let the framework handle errors automatically.

Copilot uses AI. Check for mistakes.
});
}

public readonly router: Router = Router();
}
2 changes: 2 additions & 0 deletions src/api/routes/index.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import path from 'path';
import { BusinessRouter } from './business.router';
import { CallRouter } from './call.router';
import { ChatRouter } from './chat.router';
import { ContactRouter } from './contact.router';
import { GroupRouter } from './group.router';
import { InstanceRouter } from './instance.router';
import { LabelRouter } from './label.router';
Expand Down Expand Up @@ -218,6 +219,7 @@ router
.use('/message', new MessageRouter(...guards).router)
.use('/call', new CallRouter(...guards).router)
.use('/chat', new ChatRouter(...guards).router)
.use('/contact', new ContactRouter(...guards).router)
.use('/business', new BusinessRouter(...guards).router)
.use('/group', new GroupRouter(...guards).router)
.use('/template', new TemplateRouter(configService, ...guards).router)
Expand Down
2 changes: 2 additions & 0 deletions src/api/server.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Logger } from '@config/logger.config';
import { BusinessController } from './controllers/business.controller';
import { CallController } from './controllers/call.controller';
import { ChatController } from './controllers/chat.controller';
import { ContactController } from './controllers/contact.controller';
import { GroupController } from './controllers/group.controller';
import { InstanceController } from './controllers/instance.controller';
import { LabelController } from './controllers/label.controller';
Expand Down Expand Up @@ -103,6 +104,7 @@ export const instanceController = new InstanceController(
export const sendMessageController = new SendMessageController(waMonitor);
export const callController = new CallController(waMonitor);
export const chatController = new ChatController(waMonitor);
export const contactController = new ContactController(waMonitor);
export const businessController = new BusinessController(waMonitor);
export const groupController = new GroupController(waMonitor);
export const labelController = new LabelController(waMonitor);
Expand Down
13 changes: 13 additions & 0 deletions src/validate/contact.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { JSONSchema7 } from 'json-schema';
import { v4 } from 'uuid';

export const saveContactSchema: JSONSchema7 = {
$id: v4(),
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Using a random UUID for $id can make schema identification and caching unstable.

Because $id is generated with v4(), it will change on every process start, which can break tools that rely on a stable $id for caching and cross‑schema references. Use a fixed, deterministic identifier (e.g., a URL‑like string) so the schema can be consistently referenced.

Suggested change
import { JSONSchema7 } from 'json-schema';
import { v4 } from 'uuid';
export const saveContactSchema: JSONSchema7 = {
$id: v4(),
import { JSONSchema7 } from 'json-schema';
export const saveContactSchema: JSONSchema7 = {
$id: 'https://your-app.example.com/schemas/save-contact.json',

type: 'object',
properties: {
number: { type: 'string' },
name: { type: 'string' },
saveOnDevice: { type: 'boolean', default: true },
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The 'default' keyword in JSON Schema is metadata only and won't be automatically applied by the jsonschema validation library used in this codebase. The default value is correctly handled in the Baileys service code using the nullish coalescing operator (??), so this schema property is misleading and should be removed to avoid confusion. If you want to document the default behavior, use a description field instead.

Suggested change
saveOnDevice: { type: 'boolean', default: true },
saveOnDevice: { type: 'boolean', description: 'Defaults to true when not provided.' },

Copilot uses AI. Check for mistakes.
},
required: ['number', 'name'],
Copy link
Contributor

Choose a reason for hiding this comment

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

question (bug_risk): Schema requirements and runtime behavior for name are inconsistent.

The schema makes name required, but saveContact in BaileysStartupService treats a missing data.name as valid and defaults to 'Unknown'. Please align these: either make name optional (and/or add a default) in the schema, or remove the fallback so missing name is treated as a validation error.

};
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

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

The validation schema is missing the isNotEmpty validation pattern that is consistently used throughout the codebase for required string fields. Looking at chat.schema.ts and label.schema.ts, required string fields use both the 'required' array and the isNotEmpty helper to ensure they have a minLength of 1. Without this validation, the endpoint could accept empty strings for 'number' and 'name', which would be invalid. Add the isNotEmpty helper function and apply it to the required fields.

Copilot uses AI. Check for mistakes.
Loading