diff --git a/.gitignore b/.gitignore index 10508850..e707b33e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ /dist /node_modules +/Docker/.env + # Logs logs/**.json *.log @@ -12,6 +14,7 @@ yarn-error.log* lerna-debug.log* /docker-compose-data +/docker-data # Package /yarn.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 7061bc15..a9966b51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +# 1.1.4 (2023-07-08 11:01) + +### Features + +* Route to send status broadcast +* Added verbose logs +* Insert allContacts in payload of endpoint sendStatus + +### Fixed + +* Adjusted set in webhook to go empty when enabled false +* Adjust in store files +* Fixed the problem when do not save contacts when receive messages +* Changed owner of the jid for instanceName +* Create .env for installation in docker + # 1.1.3 (2023-07-06 11:43) ### Features diff --git a/Docker/.env.example b/Docker/.env.example new file mode 100644 index 00000000..b258a83e --- /dev/null +++ b/Docker/.env.example @@ -0,0 +1,91 @@ +CORS_ORIGIN='*' # Or separate by commas - ex.: 'yourdomain1.com, yourdomain2.com' +CORS_METHODS='POST,GET,PUT,DELETE' +CORS_CREDENTIALS=true + +# Determine the logs to be displayed +LOG_LEVEL='ERROR,WARN,DEBUG,INFO,LOG,VERBOSE,DARK,WEBHOOKS' +LOG_COLOR=true +LOG_BAILEYS=error # "fatal" | "error" | "warn" | "info" | "debug" | "trace" + +# Determine how long the instance should be deleted from memory in case of no connection. +# Default time: 5 minutes +# If you don't even want an expiration, enter the value false +DEL_INSTANCE=false + +# Temporary data storage +STORE_MESSAGES=true +STORE_MESSAGE_UP=true +STORE_CONTACTS=true +STORE_CHATS=true +CLEAN_STORE_CLEANING_INTERVAL=7200 # seconds === 2h +CLEAN_STORE_MESSAGES=true +CLEAN_STORE_MESSAGE_UP=true +CLEAN_STORE_CONTACTS=true +CLEAN_STORE_CHATS=true + +# Permanent data storage +DATABASE_ENABLED=false +DATABASE_CONNECTION_URI=mongodb://root:root@mongodb:27017/?authSource=admin&readPreference=primary&ssl=false&directConnection=true +DATABASE_CONNECTION_DB_PREFIX_NAME=evolution + +# Choose the data you want to save in the application's database or store +DATABASE_SAVE_DATA_INSTANCE=true +DATABASE_SAVE_DATA_OLD_MESSAGE=false +DATABASE_SAVE_DATA_NEW_MESSAGE=true +DATABASE_SAVE_MESSAGE_UPDATE=true +DATABASE_SAVE_DATA_CONTACTS=true +DATABASE_SAVE_DATA_CHATS=true + +REDIS_ENABLED=false +REDIS_URI=redis://redis:6379/1 +REDIS_PREFIX_KEY=evolution + +# Webhook Settings +## Define a global webhook that will listen for enabled events from all instances +WEBHOOK_GLOBAL_URL='' +WEBHOOK_GLOBAL_ENABLED=false +# With this option activated, you work with a url per webhook event, respecting the global url and the name of each event +WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false +## Set the events you want to hear +WEBHOOK_EVENTS_APPLICATION_STARTUP=false +WEBHOOK_EVENTS_QRCODE_UPDATED=true +WEBHOOK_EVENTS_MESSAGES_SET=true +WEBHOOK_EVENTS_MESSAGES_UPSERT=true +WEBHOOK_EVENTS_MESSAGES_UPDATE=true +WEBHOOK_EVENTS_CONTACTS_SET=true +WEBHOOK_EVENTS_CONTACTS_UPSERT=true +WEBHOOK_EVENTS_CONTACTS_UPDATE=true +WEBHOOK_EVENTS_PRESENCE_UPDATE=true +WEBHOOK_EVENTS_CHATS_SET=true +WEBHOOK_EVENTS_CHATS_UPSERT=true +WEBHOOK_EVENTS_CHATS_UPDATE=true +WEBHOOK_EVENTS_CHATS_DELETE=true +WEBHOOK_EVENTS_GROUPS_UPSERT=true +WEBHOOK_EVENTS_GROUPS_UPDATE=true +WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=true +WEBHOOK_EVENTS_CONNECTION_UPDATE=true +# This event fires every time a new token is requested via the refresh route +WEBHOOK_EVENTS_NEW_JWT_TOKEN=true + +# Name that will be displayed on smartphone connection +CONFIG_SESSION_PHONE_CLIENT=Evolution API +CONFIG_SESSION_PHONE_NAME=chrome # chrome | firefox | edge | opera | safari + +# Set qrcode display limit +QRCODE_LIMIT=30 + +# Defines an authentication type for the api +AUTHENTICATION_TYPE='apikey' # jwt or 'apikey' +## Define a global apikey to access all instances. +### OBS: This key must be inserted in the request header to create an instance. +AUTHENTICATION_API_KEY='B6D711FCDE4D4FD5936544120E713976' +AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=true +## Set the secret key to encrypt and decrypt your token and its expiration time +AUTHENTICATION_JWT_EXPIRIN_IN=0 # seconds - 3600s ===1h | zero (0) - never expires +AUTHENTICATION_JWT_SECRET='L0YWtjb2w554WFqPG' +# Set the instance name and webhook url to create an instance in init the application +# With this option activated, you work with a url per webhook event, respecting the local url and the name of each event +AUTHENTICATION_INSTANCE_MODE=server # container or server +# if you are using container mode, set the container name and the webhook url to default instance +AUTHENTICATION_INSTANCE_NAME=evolution +AUTHENTICATION_INSTANCE_WEBHOOK_URL= \ No newline at end of file diff --git a/Docker/redis/docker-compose.yaml b/Docker/redis/docker-compose.yaml index f0524630..55e73847 100644 --- a/Docker/redis/docker-compose.yaml +++ b/Docker/redis/docker-compose.yaml @@ -7,16 +7,6 @@ networks: services: redis: image: redis:latest - command: > - redis-server - --port 6379 - --appendonly yes - --save 900 1 - --save 300 10 - --save 60 10000 - --appendfsync everysec - volumes: - - evolution_redis:/data container_name: redis ports: - 6379:6379 diff --git a/Dockerfile b/Dockerfile index 28e330e8..6fedce79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,6 +48,7 @@ ENV DATABASE_SAVE_DATA_CHATS=$DATABASE_SAVE_DATA_CHATS ENV REDIS_ENABLED=$REDIS_ENABLED ENV REDIS_URI=$REDIS_URI +ENV REDIS_PREFIX_KEY=$REDIS_PREFIX_KEY ENV WEBHOOK_GLOBAL_URL=$WEBHOOK_GLOBAL_URL ENV WEBHOOK_GLOBAL_ENABLED=$WEBHOOK_GLOBAL_ENABLED diff --git a/docker-compose.yaml b/docker-compose.yaml index ad6921e5..45345f32 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,89 +8,14 @@ services: api: container_name: evolution_api image: evolution/api:local + restart: always ports: - 8080:8080 volumes: - evolution_instances:/evolution/instances - evolution_store:/evolution/store - environment: - - LOG_LEVEL=ERROR,WARN,DEBUG,INFO,LOG,VERBOSE,DARK,WEBHOOKS - - LOG_BAILEYS=error - # Determine how long the instance should be deleted from memory in case of no connection. - # Default time: 5 minutes - # If you don't even want an expiration, enter the value false - - DEL_INSTANCE=false # 5 or false - # Temporary data storage - - STORE_MESSAGES=true - - STORE_MESSAGE_UP=true - - STORE_CONTACTS=true - - STORE_CHATS=true - - CLEAN_STORE_CLEANING_INTERVAL=7200 # seconds === 2h - - CLEAN_STORE_MESSAGES=true - - CLEAN_STORE_MESSAGE_UP=true - - CLEAN_STORE_CONTACTS=true - - CLEAN_STORE_CHATS=true - # Permanent data storage - - DATABASE_ENABLED=false - - DATABASE_CONNECTION_URI=mongodb://root:root@mongodb:27017/?authSource=admin&readPreference=primary&ssl=false&directConnection=true - - DATABASE_CONNECTION_DB_PREFIX_NAME=evolution - # Choose the data you want to save in the application's database or store - - DATABASE_SAVE_DATA_INSTANCE=true - - DATABASE_SAVE_DATA_OLD_MESSAGE=false - - DATABASE_SAVE_DATA_NEW_MESSAGE=true - - DATABASE_SAVE_MESSAGE_UPDATE=true - - DATABASE_SAVE_DATA_CONTACTS=true - - DATABASE_SAVE_DATA_CHATS=true - - REDIS_ENABLED=true - - REDIS_URI=redis://redis:6379/1 - - REDIS_PREFIX_KEY=evolution - # Webhook Settings - # Define a global webhook that will listen for enabled events from all instances - - WEBHOOK_GLOBAL_URL= - - WEBHOOK_GLOBAL_ENABLED=false - # With this option activated, you work with a url per webhook event, respecting the global url and the name of each event - - WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false - # Automatically maps webhook paths - # Set the events you want to hear - - WEBHOOK_EVENTS_APPLICATION_STARTUP=false - - WEBHOOK_EVENTS_QRCODE_UPDATED=true - - WEBHOOK_EVENTS_MESSAGES_SET=true - - WEBHOOK_EVENTS_MESSAGES_UPSERT=true - - WEBHOOK_EVENTS_MESSAGES_UPDATE=true - - WEBHOOK_EVENTS_CONTACTS_SET=true - - WEBHOOK_EVENTS_CONTACTS_UPSERT=true - - WEBHOOK_EVENTS_CONTACTS_UPDATE=true - - WEBHOOK_EVENTS_PRESENCE_UPDATE=true - - WEBHOOK_EVENTS_CHATS_SET=true - - WEBHOOK_EVENTS_CHATS_UPSERT=true - - WEBHOOK_EVENTS_CHATS_UPDATE=true - - WEBHOOK_EVENTS_CHATS_DELETE=true - - WEBHOOK_EVENTS_GROUPS_UPSERT=true - - WEBHOOK_EVENTS_GROUPS_UPDATE=true - - WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=true - - WEBHOOK_EVENTS_CONNECTION_UPDATE=true - # This event fires every time a new token is requested via the refresh route - - WEBHOOK_EVENTS_NEW_JWT_TOKEN=true - # Name that will be displayed on smartphone connection - - CONFIG_SESSION_PHONE_CLIENT=Evolution API - - CONFIG_SESSION_PHONE_NAME=chrome # chrome | firefox | edge | opera | safari - # Set qrcode display limit - - QRCODE_LIMIT=30 - # Defines an authentication type for the api - - AUTHENTICATION_TYPE=apikey # jwt or apikey - # Define a global apikey to access all instances - # OBS: This key must be inserted in the request header to create an instance. - - AUTHENTICATION_API_KEY=B6D711FCDE4D4FD5936544120E713976 - # Expose the api key on return from fetch instances - - AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=true - # Set the secret key to encrypt and decrypt your token and its expiration time. - - AUTHENTICATION_JWT_EXPIRIN_IN=0 # seconds - 3600s === 1h | zero (0) - never expires - # Set the instance name and webhook url to create an instance in init the application - # With this option activated, you work with a url per webhook event, respecting the local url and the name of each event - - AUTHENTICATION_INSTANCE_MODE=server # container or server - # if you are using container mode, set the container name and the webhook url to default instance - - AUTHENTICATION_INSTANCE_NAME=evolution - - AUTHENTICATION_INSTANCE_WEBHOOK_URL= + env_file: + - ./Docker/.env command: ['node', './dist/src/main.js'] networks: - evolution-net diff --git a/package.json b/package.json index 0e73166f..dd1b8743 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.1.3", + "version": "1.1.4", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { @@ -12,7 +12,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/DavidsonGomes/evolution-api.git" + "url": "git+https://github.com/EvolutionAPI/evolution-api.git" }, "keywords": [ "chat", @@ -36,14 +36,14 @@ }, "license": "GPL-3.0", "bugs": { - "url": "https://github.com/DavidsonGomes/evolution-api/issues" + "url": "https://github.com/EvolutionAPI/evolution-api/issues" }, - "homepage": "https://github.com/DavidsonGomes/evolution-api#readme", + "homepage": "https://github.com/EvolutionAPI/evolution-api#readme", "dependencies": { "@adiwajshing/keyed-db": "^0.2.4", "@ffmpeg-installer/ffmpeg": "^1.1.0", "@hapi/boom": "^10.0.1", - "@whiskeysockets/baileys": "^6.3.0", + "@whiskeysockets/baileys": "github:EvolutionAPI/Baileys", "axios": "^1.3.5", "class-validator": "^0.13.2", "compression": "^1.7.4", @@ -65,6 +65,7 @@ "node-cache": "^5.1.2", "node-mime-types": "^1.1.0", "pino": "^8.11.0", + "proxy-agent": "^6.2.1", "qrcode": "^1.5.1", "qrcode-terminal": "^0.12.0", "redis": "^4.6.5", diff --git a/src/config/path.config.ts b/src/config/path.config.ts index 3fb00e49..0d9b6d3c 100644 --- a/src/config/path.config.ts +++ b/src/config/path.config.ts @@ -4,3 +4,4 @@ export const ROOT_DIR = process.cwd(); export const INSTANCE_DIR = join(ROOT_DIR, 'instances'); export const SRC_DIR = join(ROOT_DIR, 'src'); export const AUTH_DIR = join(ROOT_DIR, 'store', 'auth'); +export const STORE_DIR = join(ROOT_DIR, 'store'); diff --git a/src/db/redis.client.ts b/src/db/redis.client.ts index f5725ae0..50e7efcb 100644 --- a/src/db/redis.client.ts +++ b/src/db/redis.client.ts @@ -104,9 +104,11 @@ export class RedisCache { public async delAll(hash?: string) { try { this.logger.verbose('instance delAll: ' + hash); - return await this.client.del( + const result = await this.client.del( hash || this.redisEnv.PREFIX_KEY + ':' + this.instanceName, ); + + return result; } catch (error) { this.logger.error(error); } diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 66388634..9cf411e5 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -30,7 +30,7 @@ export const instanceNameSchema: JSONSchema7 = { webhook_by_events: { type: 'boolean' }, events: { type: 'array', - minItems: 1, + minItems: 0, items: { type: 'string', enum: [ @@ -189,6 +189,37 @@ export const pollMessageSchema: JSONSchema7 = { required: ['pollMessage', 'number'], }; +export const statusMessageSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + statusMessage: { + type: 'object', + properties: { + type: { type: 'string', enum: ['text', 'image', 'audio', 'video'] }, + content: { type: 'string' }, + caption: { type: 'string' }, + backgroundColor: { type: 'string' }, + font: { type: 'integer', minimum: 0, maximum: 5 }, + statusJidList: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + pattern: '^\\d+', + description: '"statusJidList" must be an array of numeric strings', + }, + }, + allContacts: { type: 'boolean', enum: [true, false] }, + }, + required: ['type', 'content'], + ...isNotEmpty('type', 'content'), + }, + }, + required: ['statusMessage'], +}; + export const mediaMessageSchema: JSONSchema7 = { $id: v4(), type: 'object', @@ -795,7 +826,7 @@ export const webhookSchema: JSONSchema7 = { enabled: { type: 'boolean', enum: [true, false] }, events: { type: 'array', - minItems: 1, + minItems: 0, items: { type: 'string', enum: [ diff --git a/src/whatsapp/abstract/abstract.repository.ts b/src/whatsapp/abstract/abstract.repository.ts index a3afac2f..a7215383 100644 --- a/src/whatsapp/abstract/abstract.repository.ts +++ b/src/whatsapp/abstract/abstract.repository.ts @@ -6,7 +6,8 @@ import { ROOT_DIR } from '../../config/path.config'; export type IInsert = { insertCount: number }; export interface IRepository { - insert(data: any, saveDb?: boolean): Promise; + insert(data: any, instanceName: string, saveDb?: boolean): Promise; + update(data: any, instanceName: string, saveDb?: boolean): Promise; find(query: any): Promise; delete(query: any, force?: boolean): Promise; @@ -45,9 +46,14 @@ export abstract class Repository implements IRepository { } }; - public insert(data: any, saveDb = false): Promise { + public insert(data: any, instanceName: string, saveDb = false): Promise { throw new Error('Method not implemented.'); } + + public update(data: any, instanceName: string, saveDb = false): Promise { + throw new Error('Method not implemented.'); + } + public find(query: any): Promise { throw new Error('Method not implemented.'); } diff --git a/src/whatsapp/controllers/chat.controller.ts b/src/whatsapp/controllers/chat.controller.ts index 8d567a51..0a176059 100644 --- a/src/whatsapp/controllers/chat.controller.ts +++ b/src/whatsapp/controllers/chat.controller.ts @@ -16,31 +16,40 @@ import { ContactQuery } from '../repository/contact.repository'; import { MessageQuery } from '../repository/message.repository'; import { MessageUpQuery } from '../repository/messageUp.repository'; import { WAMonitoringService } from '../services/monitor.service'; +import { Logger } from '../../config/logger.config'; + +const logger = new Logger('ChatController'); export class ChatController { constructor(private readonly waMonitor: WAMonitoringService) {} public async whatsappNumber({ instanceName }: InstanceDto, data: WhatsAppNumberDto) { + logger.verbose('requested whatsappNumber from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].whatsappNumber(data); } public async readMessage({ instanceName }: InstanceDto, data: ReadMessageDto) { + logger.verbose('requested readMessage from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].markMessageAsRead(data); } public async archiveChat({ instanceName }: InstanceDto, data: ArchiveChatDto) { + logger.verbose('requested archiveChat from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].archiveChat(data); } public async deleteMessage({ instanceName }: InstanceDto, data: DeleteMessage) { + logger.verbose('requested deleteMessage from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].deleteMessage(data); } public async fetchProfilePicture({ instanceName }: InstanceDto, data: NumberDto) { + logger.verbose('requested fetchProfilePicture from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].profilePicture(data.number); } public async fetchContacts({ instanceName }: InstanceDto, query: ContactQuery) { + logger.verbose('requested fetchContacts from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].fetchContacts(query); } @@ -48,22 +57,29 @@ export class ChatController { { instanceName }: InstanceDto, data: getBase64FromMediaMessageDto, ) { + logger.verbose( + 'requested getBase64FromMediaMessage from ' + instanceName + ' instance', + ); return await this.waMonitor.waInstances[instanceName].getBase64FromMediaMessage(data); } public async fetchMessages({ instanceName }: InstanceDto, query: MessageQuery) { + logger.verbose('requested fetchMessages from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].fetchMessages(query); } public async fetchStatusMessage({ instanceName }: InstanceDto, query: MessageUpQuery) { + logger.verbose('requested fetchStatusMessage from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].fetchStatusMessage(query); } public async fetchChats({ instanceName }: InstanceDto) { + logger.verbose('requested fetchChats from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].fetchChats(); } public async fetchPrivacySettings({ instanceName }: InstanceDto) { + logger.verbose('requested fetchPrivacySettings from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].fetchPrivacySettings(); } @@ -71,6 +87,7 @@ export class ChatController { { instanceName }: InstanceDto, data: PrivacySettingDto, ) { + logger.verbose('requested updatePrivacySettings from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].updatePrivacySettings(data); } @@ -78,12 +95,14 @@ export class ChatController { { instanceName }: InstanceDto, data: ProfilePictureDto, ) { + logger.verbose('requested fetchBusinessProfile from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].fetchBusinessProfile( data.number, ); } public async updateProfileName({ instanceName }: InstanceDto, data: ProfileNameDto) { + logger.verbose('requested updateProfileName from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].updateProfileName(data.name); } @@ -91,6 +110,7 @@ export class ChatController { { instanceName }: InstanceDto, data: ProfileStatusDto, ) { + logger.verbose('requested updateProfileStatus from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].updateProfileStatus( data.status, ); @@ -100,6 +120,7 @@ export class ChatController { { instanceName }: InstanceDto, data: ProfilePictureDto, ) { + logger.verbose('requested updateProfilePicture from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].updateProfilePicture( data.picture, ); @@ -109,6 +130,7 @@ export class ChatController { { instanceName }: InstanceDto, data: ProfilePictureDto, ) { + logger.verbose('requested removeProfilePicture from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].removeProfilePicture(); } } diff --git a/src/whatsapp/controllers/group.controller.ts b/src/whatsapp/controllers/group.controller.ts index e8376bfa..e0254796 100644 --- a/src/whatsapp/controllers/group.controller.ts +++ b/src/whatsapp/controllers/group.controller.ts @@ -12,21 +12,31 @@ import { } from '../dto/group.dto'; import { InstanceDto } from '../dto/instance.dto'; import { WAMonitoringService } from '../services/monitor.service'; +import { Logger } from '../../config/logger.config'; + +const logger = new Logger('ChatController'); export class GroupController { constructor(private readonly waMonitor: WAMonitoringService) {} public async createGroup(instance: InstanceDto, create: CreateGroupDto) { + logger.verbose('requested createGroup from ' + instance.instanceName + ' instance'); return await this.waMonitor.waInstances[instance.instanceName].createGroup(create); } public async updateGroupPicture(instance: InstanceDto, update: GroupPictureDto) { + logger.verbose( + 'requested updateGroupPicture from ' + instance.instanceName + ' instance', + ); return await this.waMonitor.waInstances[instance.instanceName].updateGroupPicture( update, ); } public async updateGroupSubject(instance: InstanceDto, update: GroupSubjectDto) { + logger.verbose( + 'requested updateGroupSubject from ' + instance.instanceName + ' instance', + ); return await this.waMonitor.waInstances[instance.instanceName].updateGroupSubject( update, ); @@ -36,38 +46,54 @@ export class GroupController { instance: InstanceDto, update: GroupDescriptionDto, ) { + logger.verbose( + 'requested updateGroupDescription from ' + instance.instanceName + ' instance', + ); return await this.waMonitor.waInstances[instance.instanceName].updateGroupDescription( update, ); } public async findGroupInfo(instance: InstanceDto, groupJid: GroupJid) { + logger.verbose('requested findGroupInfo from ' + instance.instanceName + ' instance'); return await this.waMonitor.waInstances[instance.instanceName].findGroup(groupJid); } public async fetchAllGroups(instance: InstanceDto) { + logger.verbose( + 'requested fetchAllGroups from ' + instance.instanceName + ' instance', + ); return await this.waMonitor.waInstances[instance.instanceName].fetchAllGroups(); } public async inviteCode(instance: InstanceDto, groupJid: GroupJid) { + logger.verbose('requested inviteCode from ' + instance.instanceName + ' instance'); return await this.waMonitor.waInstances[instance.instanceName].inviteCode(groupJid); } public async inviteInfo(instance: InstanceDto, inviteCode: GroupInvite) { + logger.verbose('requested inviteInfo from ' + instance.instanceName + ' instance'); return await this.waMonitor.waInstances[instance.instanceName].inviteInfo(inviteCode); } public async sendInvite(instance: InstanceDto, data: GroupSendInvite) { + logger.verbose('requested sendInvite from ' + instance.instanceName + ' instance'); return await this.waMonitor.waInstances[instance.instanceName].sendInvite(data); } public async revokeInviteCode(instance: InstanceDto, groupJid: GroupJid) { + logger.verbose( + 'requested revokeInviteCode from ' + instance.instanceName + ' instance', + ); return await this.waMonitor.waInstances[instance.instanceName].revokeInviteCode( groupJid, ); } public async findParticipants(instance: InstanceDto, groupJid: GroupJid) { + logger.verbose( + 'requested findParticipants from ' + instance.instanceName + ' instance', + ); return await this.waMonitor.waInstances[instance.instanceName].findParticipants( groupJid, ); @@ -77,22 +103,32 @@ export class GroupController { instance: InstanceDto, update: GroupUpdateParticipantDto, ) { + logger.verbose( + 'requested updateGParticipate from ' + instance.instanceName + ' instance', + ); return await this.waMonitor.waInstances[instance.instanceName].updateGParticipant( update, ); } public async updateGSetting(instance: InstanceDto, update: GroupUpdateSettingDto) { + logger.verbose( + 'requested updateGSetting from ' + instance.instanceName + ' instance', + ); return await this.waMonitor.waInstances[instance.instanceName].updateGSetting(update); } public async toggleEphemeral(instance: InstanceDto, update: GroupToggleEphemeralDto) { + logger.verbose( + 'requested toggleEphemeral from ' + instance.instanceName + ' instance', + ); return await this.waMonitor.waInstances[instance.instanceName].toggleEphemeral( update, ); } public async leaveGroup(instance: InstanceDto, groupJid: GroupJid) { + logger.verbose('requested leaveGroup from ' + instance.instanceName + ' instance'); return await this.waMonitor.waInstances[instance.instanceName].leaveGroup(groupJid); } } diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index b309366a..c2abbffa 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -33,9 +33,13 @@ export class InstanceController { qrcode, token, }: InstanceDto) { + this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); + const mode = this.configService.get('AUTHENTICATION').INSTANCE.MODE; if (mode === 'container') { + this.logger.verbose('container mode'); + if (Object.keys(this.waMonitor.waInstances).length > 0) { throw new BadRequestException([ 'Instance already created', @@ -43,8 +47,10 @@ export class InstanceController { ]); } + this.logger.verbose('checking duplicate token'); await this.authService.checkDuplicateToken(token); + this.logger.verbose('creating instance'); const instance = new WAStartupService( this.configService, this.eventEmitter, @@ -52,9 +58,12 @@ export class InstanceController { this.cache, ); instance.instanceName = instanceName; + this.logger.verbose('instance: ' + instance.instanceName + ' created'); + this.waMonitor.waInstances[instance.instanceName] = instance; this.waMonitor.delInstanceTime(instance.instanceName); + this.logger.verbose('generating hash'); const hash = await this.authService.generateHash( { instanceName: instance.instanceName, @@ -62,9 +71,12 @@ export class InstanceController { token, ); + this.logger.verbose('hash: ' + hash + ' generated'); + let getEvents: string[]; if (webhook) { + this.logger.verbose('creating webhook'); try { this.webhookService.create(instance, { enabled: true, @@ -79,6 +91,17 @@ export class InstanceController { } } + this.logger.verbose('instance created'); + this.logger.verbose({ + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, + webhook, + events: getEvents, + }); + return { instance: { instanceName: instance.instanceName, @@ -89,8 +112,12 @@ export class InstanceController { events: getEvents, }; } else { + this.logger.verbose('server mode'); + + this.logger.verbose('checking duplicate token'); await this.authService.checkDuplicateToken(token); + this.logger.verbose('creating instance'); const instance = new WAStartupService( this.configService, this.eventEmitter, @@ -98,9 +125,13 @@ export class InstanceController { this.cache, ); instance.instanceName = instanceName; + + this.logger.verbose('instance: ' + instance.instanceName + ' created'); + this.waMonitor.waInstances[instance.instanceName] = instance; this.waMonitor.delInstanceTime(instance.instanceName); + this.logger.verbose('generating hash'); const hash = await this.authService.generateHash( { instanceName: instance.instanceName, @@ -108,9 +139,12 @@ export class InstanceController { token, ); + this.logger.verbose('hash: ' + hash + ' generated'); + let getEvents: string[]; if (webhook) { + this.logger.verbose('creating webhook'); try { this.webhookService.create(instance, { enabled: true, @@ -128,11 +162,25 @@ export class InstanceController { let getQrcode: wa.QrCode; if (qrcode) { + this.logger.verbose('creating qrcode'); await instance.connectToWhatsapp(); await delay(2000); getQrcode = instance.qrCode; } + this.logger.verbose('instance created'); + this.logger.verbose({ + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, + webhook, + webhook_by_events, + events: getEvents, + qrcode: getQrcode, + }); + return { instance: { instanceName: instance.instanceName, @@ -149,11 +197,18 @@ export class InstanceController { public async connectToWhatsapp({ instanceName }: InstanceDto) { try { + this.logger.verbose( + 'requested connectToWhatsapp from ' + instanceName + ' instance', + ); + const instance = this.waMonitor.waInstances[instanceName]; const state = instance?.connectionStatus?.state; + this.logger.verbose('state: ' + state); + switch (state) { case 'close': + this.logger.verbose('connecting'); await instance.connectToWhatsapp(); await delay(2000); return instance.qrCode; @@ -169,8 +224,12 @@ export class InstanceController { public async restartInstance({ instanceName }: InstanceDto) { try { + this.logger.verbose('requested restartInstance from ' + instanceName + ' instance'); + + this.logger.verbose('deleting instance: ' + instanceName); delete this.waMonitor.waInstances[instanceName]; - console.log(this.waMonitor.waInstances[instanceName]); + + this.logger.verbose('creating instance: ' + instanceName); const instance = new WAStartupService( this.configService, this.eventEmitter, @@ -179,6 +238,10 @@ export class InstanceController { ); instance.instanceName = instanceName; + + this.logger.verbose('instance: ' + instance.instanceName + ' created'); + + this.logger.verbose('connecting instance: ' + instanceName); await instance.connectToWhatsapp(); this.waMonitor.waInstances[instance.instanceName] = instance; @@ -189,11 +252,14 @@ export class InstanceController { } public async connectionState({ instanceName }: InstanceDto) { + this.logger.verbose('requested connectionState from ' + instanceName + ' instance'); return this.waMonitor.waInstances[instanceName]?.connectionStatus; } public async fetchInstances({ instanceName }: InstanceDto) { + this.logger.verbose('requested fetchInstances from ' + instanceName + ' instance'); if (instanceName) { + this.logger.verbose('instanceName: ' + instanceName); return this.waMonitor.instanceInfo(instanceName); } @@ -201,11 +267,14 @@ export class InstanceController { } public async logout({ instanceName }: InstanceDto) { + this.logger.verbose('requested logout from ' + instanceName + ' instance'); try { + this.logger.verbose('logging out instance: ' + instanceName); await this.waMonitor.waInstances[instanceName]?.client?.logout( 'Log out instance: ' + instanceName, ); + this.logger.verbose('close connection instance: ' + instanceName); this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); return { error: false, message: 'Instance logged out' }; @@ -215,7 +284,9 @@ export class InstanceController { } public async deleteInstance({ instanceName }: InstanceDto) { + this.logger.verbose('requested deleteInstance from ' + instanceName + ' instance'); const stateConn = await this.connectionState({ instanceName }); + if (stateConn.state === 'open') { throw new BadRequestException([ 'Deletion failed', @@ -224,10 +295,14 @@ export class InstanceController { } try { if (stateConn.state === 'connecting') { + this.logger.verbose('logging out instance: ' + instanceName); + await this.logout({ instanceName }); delete this.waMonitor.waInstances[instanceName]; return { error: false, message: 'Instance deleted' }; } else { + this.logger.verbose('deleting instance: ' + instanceName); + delete this.waMonitor.waInstances[instanceName]; this.eventEmitter.emit('remove.instance', instanceName, 'inner'); return { error: false, message: 'Instance deleted' }; @@ -238,6 +313,7 @@ export class InstanceController { } public async refreshToken(_: InstanceDto, oldToken: OldToken) { + this.logger.verbose('requested refreshToken'); return await this.authService.refreshToken(oldToken); } } diff --git a/src/whatsapp/controllers/sendMessage.controller.ts b/src/whatsapp/controllers/sendMessage.controller.ts index b985d5e7..c2d5298c 100644 --- a/src/whatsapp/controllers/sendMessage.controller.ts +++ b/src/whatsapp/controllers/sendMessage.controller.ts @@ -11,22 +11,35 @@ import { SendMediaDto, SendPollDto, SendReactionDto, + SendStatusDto, SendStickerDto, SendTextDto, } from '../dto/sendMessage.dto'; import { WAMonitoringService } from '../services/monitor.service'; +import { Logger } from '../../config/logger.config'; + +const logger = new Logger('MessageRouter'); + export class SendMessageController { constructor(private readonly waMonitor: WAMonitoringService) {} public async sendText({ instanceName }: InstanceDto, data: SendTextDto) { + logger.verbose('requested sendText from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].textMessage(data); } public async sendMedia({ instanceName }: InstanceDto, data: SendMediaDto) { + logger.verbose('requested sendMedia from ' + instanceName + ' instance'); if (isBase64(data?.mediaMessage?.media) && !data?.mediaMessage?.fileName) { throw new BadRequestException('For bse64 the file name must be informed.'); } + logger.verbose( + 'isURL: ' + + isURL(data?.mediaMessage?.media) + + ', isBase64: ' + + isBase64(data?.mediaMessage?.media), + ); if (isURL(data?.mediaMessage?.media) || isBase64(data?.mediaMessage?.media)) { return await this.waMonitor.waInstances[instanceName].mediaMessage(data); } @@ -34,6 +47,14 @@ export class SendMessageController { } public async sendSticker({ instanceName }: InstanceDto, data: SendStickerDto) { + logger.verbose('requested sendSticker from ' + instanceName + ' instance'); + + logger.verbose( + 'isURL: ' + + isURL(data?.stickerMessage?.image) + + ', isBase64: ' + + isBase64(data?.stickerMessage?.image), + ); if (isURL(data.stickerMessage.image) || isBase64(data.stickerMessage.image)) { return await this.waMonitor.waInstances[instanceName].mediaSticker(data); } @@ -41,6 +62,14 @@ export class SendMessageController { } public async sendWhatsAppAudio({ instanceName }: InstanceDto, data: SendAudioDto) { + logger.verbose('requested sendWhatsAppAudio from ' + instanceName + ' instance'); + + logger.verbose( + 'isURL: ' + + isURL(data?.audioMessage?.audio) + + ', isBase64: ' + + isBase64(data?.audioMessage?.audio), + ); if (isURL(data.audioMessage.audio) || isBase64(data.audioMessage.audio)) { return await this.waMonitor.waInstances[instanceName].audioWhatsapp(data); } @@ -48,6 +77,7 @@ export class SendMessageController { } public async sendButtons({ instanceName }: InstanceDto, data: SendButtonDto) { + logger.verbose('requested sendButtons from ' + instanceName + ' instance'); if ( isBase64(data.buttonMessage.mediaMessage?.media) && !data.buttonMessage.mediaMessage?.fileName @@ -58,18 +88,22 @@ export class SendMessageController { } public async sendLocation({ instanceName }: InstanceDto, data: SendLocationDto) { + logger.verbose('requested sendLocation from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].locationMessage(data); } public async sendList({ instanceName }: InstanceDto, data: SendListDto) { + logger.verbose('requested sendList from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].listMessage(data); } public async sendContact({ instanceName }: InstanceDto, data: SendContactDto) { + logger.verbose('requested sendContact from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].contactMessage(data); } public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) { + logger.verbose('requested sendReaction from ' + instanceName + ' instance'); if (!data.reactionMessage.reaction.match(/[^\(\)\w\sà-ú"-\+]+/)) { throw new BadRequestException('"reaction" must be an emoji'); } @@ -77,10 +111,17 @@ export class SendMessageController { } public async sendPoll({ instanceName }: InstanceDto, data: SendPollDto) { + logger.verbose('requested sendPoll from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].pollMessage(data); } + public async sendStatus({ instanceName }: InstanceDto, data: SendStatusDto) { + logger.verbose('requested sendStatus from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].statusMessage(data); + } + public async sendLinkPreview({ instanceName }: InstanceDto, data: SendLinkPreviewDto) { + logger.verbose('requested sendLinkPreview from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].linkPreview(data); } } diff --git a/src/whatsapp/controllers/webhook.controller.ts b/src/whatsapp/controllers/webhook.controller.ts index ec6dbb3d..b5747b2e 100644 --- a/src/whatsapp/controllers/webhook.controller.ts +++ b/src/whatsapp/controllers/webhook.controller.ts @@ -3,18 +3,31 @@ import { BadRequestException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { WebhookDto } from '../dto/webhook.dto'; import { WebhookService } from '../services/webhook.service'; +import { Logger } from '../../config/logger.config'; + +const logger = new Logger('WebhookController'); export class WebhookController { constructor(private readonly webhookService: WebhookService) {} public async createWebhook(instance: InstanceDto, data: WebhookDto) { - if (!isURL(data.url, { require_tld: false })) { + logger.verbose('requested createWebhook from ' + instance.instanceName + ' instance'); + + if (data.enabled && !isURL(data.url, { require_tld: false })) { throw new BadRequestException('Invalid "url" property'); } + + if (!data.enabled) { + logger.verbose('webhook disabled'); + data.url = ''; + data.events = []; + } + return this.webhookService.create(instance, data); } public async findWebhook(instance: InstanceDto) { + logger.verbose('requested findWebhook from ' + instance.instanceName + ' instance'); return this.webhookService.find(instance); } } diff --git a/src/whatsapp/dto/sendMessage.dto.ts b/src/whatsapp/dto/sendMessage.dto.ts index 0d3f7183..4f1ff88b 100644 --- a/src/whatsapp/dto/sendMessage.dto.ts +++ b/src/whatsapp/dto/sendMessage.dto.ts @@ -32,6 +32,16 @@ class linkPreviewMessage { text: string; } +export class StatusMessage { + type: string; + content: string; + statusJidList?: string[]; + allContacts?: boolean; + caption?: string; + backgroundColor?: string; + font?: number; +} + class PollMessage { name: string; selectableCount: number; @@ -46,6 +56,10 @@ export class SendLinkPreviewDto extends Metadata { linkPreview: linkPreviewMessage; } +export class SendStatusDto extends Metadata { + statusMessage: StatusMessage; +} + export class SendPollDto extends Metadata { pollMessage: PollMessage; } diff --git a/src/whatsapp/repository/auth.repository.ts b/src/whatsapp/repository/auth.repository.ts index 4b6d4d00..c795737c 100644 --- a/src/whatsapp/repository/auth.repository.ts +++ b/src/whatsapp/repository/auth.repository.ts @@ -4,7 +4,7 @@ import { IInsert, Repository } from '../abstract/abstract.repository'; import { IAuthModel, AuthRaw } from '../models'; import { readFileSync } from 'fs'; import { AUTH_DIR } from '../../config/path.config'; -import { InstanceDto } from '../dto/instance.dto'; +import { Logger } from '../../config/logger.config'; export class AuthRepository extends Repository { constructor( @@ -16,24 +16,35 @@ export class AuthRepository extends Repository { } private readonly auth: Auth; + private readonly logger = new Logger('AuthRepository'); public async create(data: AuthRaw, instance: string): Promise { try { + this.logger.verbose('creating auth'); if (this.dbSettings.ENABLED) { + this.logger.verbose('saving auth to db'); const insert = await this.authModel.replaceOne( { _id: instance }, { ...data }, { upsert: true }, ); + + this.logger.verbose('auth saved to db: ' + insert.modifiedCount + ' auth'); return { insertCount: insert.modifiedCount }; } + this.logger.verbose('saving auth to store'); + this.writeStore({ path: join(AUTH_DIR, this.auth.TYPE), fileName: instance, data, }); + this.logger.verbose( + 'auth saved to store in path: ' + join(AUTH_DIR, this.auth.TYPE) + '/' + instance, + ); + this.logger.verbose('auth created'); return { insertCount: 1 }; } catch (error) { return { error } as any; @@ -42,10 +53,14 @@ export class AuthRepository extends Repository { public async find(instance: string): Promise { try { + this.logger.verbose('finding auth'); if (this.dbSettings.ENABLED) { + this.logger.verbose('finding auth in db'); return await this.authModel.findOne({ _id: instance }); } + this.logger.verbose('finding auth in store'); + return JSON.parse( readFileSync(join(AUTH_DIR, this.auth.TYPE, instance + '.json'), { encoding: 'utf-8', diff --git a/src/whatsapp/repository/chat.repository.ts b/src/whatsapp/repository/chat.repository.ts index d57a9ea2..0f05760c 100644 --- a/src/whatsapp/repository/chat.repository.ts +++ b/src/whatsapp/repository/chat.repository.ts @@ -3,6 +3,7 @@ import { ConfigService, StoreConf } from '../../config/env.config'; import { IInsert, Repository } from '../abstract/abstract.repository'; import { opendirSync, readFileSync, rmSync } from 'fs'; import { ChatRaw, IChatModel } from '../models'; +import { Logger } from '../../config/logger.config'; export class ChatQuery { where: ChatRaw; @@ -16,31 +17,54 @@ export class ChatRepository extends Repository { super(configService); } - public async insert(data: ChatRaw[], saveDb = false): Promise { + private readonly logger = new Logger('ChatRepository'); + + public async insert( + data: ChatRaw[], + instanceName: string, + saveDb = false, + ): Promise { + this.logger.verbose('inserting chats'); if (data.length === 0) { + this.logger.verbose('no chats to insert'); return; } try { + this.logger.verbose('saving chats to store'); if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('saving chats to db'); const insert = await this.chatModel.insertMany([...data]); + + this.logger.verbose('chats saved to db: ' + insert.length + ' chats'); return { insertCount: insert.length }; } + this.logger.verbose('saving chats to store'); + const store = this.configService.get('STORE'); if (store.CHATS) { + this.logger.verbose('saving chats to store'); data.forEach((chat) => { this.writeStore({ - path: join(this.storePath, 'chats', chat.owner), + path: join(this.storePath, 'chats', instanceName), fileName: chat.id, data: chat, }); + this.logger.verbose( + 'chats saved to store in path: ' + + join(this.storePath, 'chats', instanceName) + + '/' + + chat.id, + ); }); + this.logger.verbose('chats saved to store'); return { insertCount: data.length }; } + this.logger.verbose('chats not saved to store'); return { insertCount: 0 }; } catch (error) { return error; @@ -51,10 +75,14 @@ export class ChatRepository extends Repository { public async find(query: ChatQuery): Promise { try { + this.logger.verbose('finding chats'); if (this.dbSettings.ENABLED) { + this.logger.verbose('finding chats in db'); return await this.chatModel.find({ owner: query.where.owner }); } + this.logger.verbose('finding chats in store'); + const chats: ChatRaw[] = []; const openDir = opendirSync(join(this.storePath, 'chats', query.where.owner)); for await (const dirent of openDir) { @@ -70,6 +98,7 @@ export class ChatRepository extends Repository { } } + this.logger.verbose('chats found in store: ' + chats.length + ' chats'); return chats; } catch (error) { return []; @@ -78,10 +107,13 @@ export class ChatRepository extends Repository { public async delete(query: ChatQuery) { try { + this.logger.verbose('deleting chats'); if (this.dbSettings.ENABLED) { + this.logger.verbose('deleting chats in db'); return await this.chatModel.deleteOne({ ...query.where }); } + this.logger.verbose('deleting chats in store'); rmSync(join(this.storePath, 'chats', query.where.owner, query.where.id + '.josn'), { force: true, recursive: true, diff --git a/src/whatsapp/repository/contact.repository.ts b/src/whatsapp/repository/contact.repository.ts index 26dfc25c..648b5bf4 100644 --- a/src/whatsapp/repository/contact.repository.ts +++ b/src/whatsapp/repository/contact.repository.ts @@ -3,6 +3,7 @@ import { join } from 'path'; import { ConfigService, StoreConf } from '../../config/env.config'; import { ContactRaw, IContactModel } from '../models'; import { IInsert, Repository } from '../abstract/abstract.repository'; +import { Logger } from '../../config/logger.config'; export class ContactQuery { where: ContactRaw; @@ -16,31 +17,121 @@ export class ContactRepository extends Repository { super(configService); } - public async insert(data: ContactRaw[], saveDb = false): Promise { + private readonly logger = new Logger('ContactRepository'); + + public async insert( + data: ContactRaw[], + instanceName: string, + saveDb = false, + ): Promise { + this.logger.verbose('inserting contacts'); + if (data.length === 0) { + this.logger.verbose('no contacts to insert'); return; } try { if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('saving contacts to db'); + const insert = await this.contactModel.insertMany([...data]); + + this.logger.verbose('contacts saved to db: ' + insert.length + ' contacts'); return { insertCount: insert.length }; } + this.logger.verbose('saving contacts to store'); + const store = this.configService.get('STORE'); if (store.CONTACTS) { + this.logger.verbose('saving contacts to store'); data.forEach((contact) => { this.writeStore({ - path: join(this.storePath, 'contacts', contact.owner), + path: join(this.storePath, 'contacts', instanceName), fileName: contact.id, data: contact, }); + this.logger.verbose( + 'contacts saved to store in path: ' + + join(this.storePath, 'contacts', instanceName) + + '/' + + contact.id, + ); }); + this.logger.verbose('contacts saved to store: ' + data.length + ' contacts'); + return { insertCount: data.length }; + } + + this.logger.verbose('contacts not saved'); + return { insertCount: 0 }; + } catch (error) { + return error; + } finally { + data = undefined; + } + } + + public async update( + data: ContactRaw[], + instanceName: string, + saveDb = false, + ): Promise { + try { + this.logger.verbose('updating contacts'); + + if (data.length === 0) { + this.logger.verbose('no contacts to update'); + return; + } + + if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('updating contacts in db'); + + const contacts = data.map((contact) => { + return { + updateOne: { + filter: { id: contact.id }, + update: { ...contact }, + upsert: true, + }, + }; + }); + + const { nModified } = await this.contactModel.bulkWrite(contacts); + + this.logger.verbose('contacts updated in db: ' + nModified + ' contacts'); + return { insertCount: nModified }; + } + + this.logger.verbose('updating contacts in store'); + + const store = this.configService.get('STORE'); + + if (store.CONTACTS) { + this.logger.verbose('updating contacts in store'); + data.forEach((contact) => { + this.writeStore({ + path: join(this.storePath, 'contacts', instanceName), + fileName: contact.id, + data: contact, + }); + this.logger.verbose( + 'contacts updated in store in path: ' + + join(this.storePath, 'contacts', instanceName) + + '/' + + contact.id, + ); + }); + + this.logger.verbose('contacts updated in store: ' + data.length + ' contacts'); + return { insertCount: data.length }; } + this.logger.verbose('contacts not updated'); return { insertCount: 0 }; } catch (error) { return error; @@ -51,11 +142,16 @@ export class ContactRepository extends Repository { public async find(query: ContactQuery): Promise { try { + this.logger.verbose('finding contacts'); if (this.dbSettings.ENABLED) { + this.logger.verbose('finding contacts in db'); return await this.contactModel.find({ ...query.where }); } + + this.logger.verbose('finding contacts in store'); const contacts: ContactRaw[] = []; if (query?.where?.id) { + this.logger.verbose('finding contacts in store by id'); contacts.push( JSON.parse( readFileSync( @@ -70,6 +166,8 @@ export class ContactRepository extends Repository { ), ); } else { + this.logger.verbose('finding contacts in store by owner'); + const openDir = opendirSync(join(this.storePath, 'contacts', query.where.owner), { encoding: 'utf-8', }); @@ -86,6 +184,8 @@ export class ContactRepository extends Repository { } } } + + this.logger.verbose('contacts found in store: ' + contacts.length + ' contacts'); return contacts; } catch (error) { return []; diff --git a/src/whatsapp/repository/message.repository.ts b/src/whatsapp/repository/message.repository.ts index eeb207af..dbfe01fc 100644 --- a/src/whatsapp/repository/message.repository.ts +++ b/src/whatsapp/repository/message.repository.ts @@ -3,6 +3,7 @@ import { join } from 'path'; import { IMessageModel, MessageRaw } from '../models'; import { IInsert, Repository } from '../abstract/abstract.repository'; import { opendirSync, readFileSync } from 'fs'; +import { Logger } from '../../config/logger.config'; export class MessageQuery { where: MessageRaw; @@ -17,13 +18,23 @@ export class MessageRepository extends Repository { super(configService); } - public async insert(data: MessageRaw[], saveDb = false): Promise { + private readonly logger = new Logger('MessageRepository'); + + public async insert( + data: MessageRaw[], + instanceName: string, + saveDb = false, + ): Promise { + this.logger.verbose('inserting messages'); + if (!Array.isArray(data) || data.length === 0) { + this.logger.verbose('no messages to insert'); return; } try { if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('saving messages to db'); const cleanedData = data.map((obj) => { const cleanedObj = { ...obj }; if ('extendedTextMessage' in obj.message) { @@ -44,23 +55,37 @@ export class MessageRepository extends Repository { }); const insert = await this.messageModel.insertMany([...cleanedData]); + + this.logger.verbose('messages saved to db: ' + insert.length + ' messages'); return { insertCount: insert.length }; } + this.logger.verbose('saving messages to store'); + const store = this.configService.get('STORE'); if (store.MESSAGES) { - data.forEach((msg) => - this.writeStore({ - path: join(this.storePath, 'messages', msg.owner), - fileName: msg.key.id, - data: msg, - }), - ); + this.logger.verbose('saving messages to store'); + data.forEach((message) => { + this.writeStore({ + path: join(this.storePath, 'messages', instanceName), + fileName: message.key.id, + data: message, + }); + this.logger.verbose( + 'messages saved to store in path: ' + + join(this.storePath, 'messages', instanceName) + + '/' + + message.key.id, + ); + }); + + this.logger.verbose('messages saved to store: ' + data.length + ' messages'); return { insertCount: data.length }; } + this.logger.verbose('messages not saved to store'); return { insertCount: 0 }; } catch (error) { console.log('ERROR: ', error); @@ -72,21 +97,26 @@ export class MessageRepository extends Repository { public async find(query: MessageQuery) { try { + this.logger.verbose('finding messages'); if (this.dbSettings.ENABLED) { + this.logger.verbose('finding messages in db'); if (query?.where?.key) { for (const [k, v] of Object.entries(query.where.key)) { query.where['key.' + k] = v; } delete query?.where?.key; } + return await this.messageModel .find({ ...query.where }) .sort({ messageTimestamp: -1 }) .limit(query?.limit ?? 0); } + this.logger.verbose('finding messages in store'); const messages: MessageRaw[] = []; if (query?.where?.key?.id) { + this.logger.verbose('finding messages in store by id'); messages.push( JSON.parse( readFileSync( @@ -101,6 +131,7 @@ export class MessageRepository extends Repository { ), ); } else { + this.logger.verbose('finding messages in store by owner'); const openDir = opendirSync(join(this.storePath, 'messages', query.where.owner), { encoding: 'utf-8', }); @@ -119,6 +150,7 @@ export class MessageRepository extends Repository { } } + this.logger.verbose('messages found in store: ' + messages.length + ' messages'); return messages .sort((x, y) => { return (y.messageTimestamp as number) - (x.messageTimestamp as number); diff --git a/src/whatsapp/repository/messageUp.repository.ts b/src/whatsapp/repository/messageUp.repository.ts index e40fe1c9..6a9f9cc4 100644 --- a/src/whatsapp/repository/messageUp.repository.ts +++ b/src/whatsapp/repository/messageUp.repository.ts @@ -3,6 +3,7 @@ import { IMessageUpModel, MessageUpdateRaw } from '../models'; import { IInsert, Repository } from '../abstract/abstract.repository'; import { join } from 'path'; import { opendirSync, readFileSync } from 'fs'; +import { Logger } from '../../config/logger.config'; export class MessageUpQuery { where: MessageUpdateRaw; @@ -17,31 +18,54 @@ export class MessageUpRepository extends Repository { super(configService); } - public async insert(data: MessageUpdateRaw[], saveDb?: boolean): Promise { + private readonly logger = new Logger('MessageUpRepository'); + + public async insert( + data: MessageUpdateRaw[], + instanceName: string, + saveDb?: boolean, + ): Promise { + this.logger.verbose('inserting message up'); + if (data.length === 0) { + this.logger.verbose('no message up to insert'); return; } try { if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('saving message up to db'); const insert = await this.messageUpModel.insertMany([...data]); + + this.logger.verbose('message up saved to db: ' + insert.length + ' message up'); return { insertCount: insert.length }; } + this.logger.verbose('saving message up to store'); + const store = this.configService.get('STORE'); if (store.MESSAGE_UP) { + this.logger.verbose('saving message up to store'); data.forEach((update) => { this.writeStore({ - path: join(this.storePath, 'message-up', update.owner), + path: join(this.storePath, 'message-up', instanceName), fileName: update.id, data: update, }); + this.logger.verbose( + 'message up saved to store in path: ' + + join(this.storePath, 'message-up', instanceName) + + '/' + + update.id, + ); }); + this.logger.verbose('message up saved to store: ' + data.length + ' message up'); return { insertCount: data.length }; } + this.logger.verbose('message up not saved to store'); return { insertCount: 0 }; } catch (error) { return error; @@ -50,15 +74,21 @@ export class MessageUpRepository extends Repository { public async find(query: MessageUpQuery) { try { + this.logger.verbose('finding message up'); if (this.dbSettings.ENABLED) { + this.logger.verbose('finding message up in db'); return await this.messageUpModel .find({ ...query.where }) .sort({ datetime: -1 }) .limit(query?.limit ?? 0); } + this.logger.verbose('finding message up in store'); + const messageUpdate: MessageUpdateRaw[] = []; if (query?.where?.id) { + this.logger.verbose('finding message up in store by id'); + messageUpdate.push( JSON.parse( readFileSync( @@ -73,6 +103,8 @@ export class MessageUpRepository extends Repository { ), ); } else { + this.logger.verbose('finding message up in store by owner'); + const openDir = opendirSync( join(this.storePath, 'message-up', query.where.owner), { encoding: 'utf-8' }, @@ -92,6 +124,9 @@ export class MessageUpRepository extends Repository { } } + this.logger.verbose( + 'message up found in store: ' + messageUpdate.length + ' message up', + ); return messageUpdate .sort((x, y) => { return y.datetime - x.datetime; diff --git a/src/whatsapp/repository/repository.manager.ts b/src/whatsapp/repository/repository.manager.ts index eac7b18e..1e08213a 100644 --- a/src/whatsapp/repository/repository.manager.ts +++ b/src/whatsapp/repository/repository.manager.ts @@ -8,6 +8,7 @@ import { AuthRepository } from './auth.repository'; import { Auth, ConfigService, Database } from '../../config/env.config'; import { execSync } from 'child_process'; import { join } from 'path'; +import { Logger } from '../../config/logger.config'; export class RepositoryBroker { constructor( @@ -20,19 +21,26 @@ export class RepositoryBroker { private configService: ConfigService, dbServer?: MongoClient, ) { + this.logger.verbose('initializing repository broker'); this.dbClient = dbServer; this.__init_repo_without_db__(); } private dbClient?: MongoClient; + private readonly logger = new Logger('RepositoryBroker'); public get dbServer() { return this.dbClient; } private __init_repo_without_db__() { + this.logger.verbose('initializing repository without db'); if (!this.configService.get('DATABASE').ENABLED) { + this.logger.verbose('database is disabled'); + const storePath = join(process.cwd(), 'store'); + + this.logger.verbose('creating store path: ' + storePath); execSync( `mkdir -p ${join( storePath, @@ -40,10 +48,20 @@ export class RepositoryBroker { this.configService.get('AUTHENTICATION').TYPE, )}`, ); + + this.logger.verbose('creating chats path: ' + join(storePath, 'chats')); execSync(`mkdir -p ${join(storePath, 'chats')}`); + + this.logger.verbose('creating contacts path: ' + join(storePath, 'contacts')); execSync(`mkdir -p ${join(storePath, 'contacts')}`); + + this.logger.verbose('creating messages path: ' + join(storePath, 'messages')); execSync(`mkdir -p ${join(storePath, 'messages')}`); + + this.logger.verbose('creating message-up path: ' + join(storePath, 'message-up')); execSync(`mkdir -p ${join(storePath, 'message-up')}`); + + this.logger.verbose('creating webhook path: ' + join(storePath, 'webhook')); execSync(`mkdir -p ${join(storePath, 'webhook')}`); } } diff --git a/src/whatsapp/repository/webhook.repository.ts b/src/whatsapp/repository/webhook.repository.ts index 2a0c12a4..d9b34af1 100644 --- a/src/whatsapp/repository/webhook.repository.ts +++ b/src/whatsapp/repository/webhook.repository.ts @@ -3,6 +3,7 @@ import { ConfigService } from '../../config/env.config'; import { join } from 'path'; import { readFileSync } from 'fs'; import { IWebhookModel, WebhookRaw } from '../models'; +import { Logger } from '../../config/logger.config'; export class WebhookRepository extends Repository { constructor( @@ -12,23 +13,39 @@ export class WebhookRepository extends Repository { super(configService); } + private readonly logger = new Logger('WebhookRepository'); + public async create(data: WebhookRaw, instance: string): Promise { try { + this.logger.verbose('creating webhook'); if (this.dbSettings.ENABLED) { + this.logger.verbose('saving webhook to db'); const insert = await this.webhookModel.replaceOne( { _id: instance }, { ...data }, { upsert: true }, ); + + this.logger.verbose('webhook saved to db: ' + insert.modifiedCount + ' webhook'); return { insertCount: insert.modifiedCount }; } + this.logger.verbose('saving webhook to store'); + this.writeStore({ path: join(this.storePath, 'webhook'), fileName: instance, data, }); + this.logger.verbose( + 'webhook saved to store in path: ' + + join(this.storePath, 'webhook') + + '/' + + instance, + ); + + this.logger.verbose('webhook created'); return { insertCount: 1 }; } catch (error) { return error; @@ -37,10 +54,13 @@ export class WebhookRepository extends Repository { public async find(instance: string): Promise { try { + this.logger.verbose('finding webhook'); if (this.dbSettings.ENABLED) { + this.logger.verbose('finding webhook in db'); return await this.webhookModel.findOne({ _id: instance }); } + this.logger.verbose('finding webhook in store'); return JSON.parse( readFileSync(join(this.storePath, 'webhook', instance + '.json'), { encoding: 'utf-8', diff --git a/src/whatsapp/routers/chat.router.ts b/src/whatsapp/routers/chat.router.ts index 46d05aa9..50ead521 100644 --- a/src/whatsapp/routers/chat.router.ts +++ b/src/whatsapp/routers/chat.router.ts @@ -32,12 +32,22 @@ import { HttpStatus } from './index.router'; import { MessageUpQuery } from '../repository/messageUp.repository'; import { proto } from '@whiskeysockets/baileys'; import { InstanceDto } from '../dto/instance.dto'; +import { Logger } from '../../config/logger.config'; + +const logger = new Logger('ChatRouter'); export class ChatRouter extends RouterBroker { constructor(...guards: RequestHandler[]) { super(); this.router .post(this.routerPath('whatsappNumbers'), ...guards, async (req, res) => { + logger.verbose('request received in whatsappNumbers'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: whatsappNumberSchema, @@ -48,6 +58,13 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) .put(this.routerPath('markMessageAsRead'), ...guards, async (req, res) => { + logger.verbose('request received in markMessageAsRead'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: readMessageSchema, @@ -58,6 +75,13 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) .put(this.routerPath('archiveChat'), ...guards, async (req, res) => { + logger.verbose('request received in archiveChat'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: archiveChatSchema, @@ -71,6 +95,13 @@ export class ChatRouter extends RouterBroker { this.routerPath('deleteMessageForEveryone'), ...guards, async (req, res) => { + logger.verbose('request received in deleteMessageForEveryone'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: deleteMessageSchema, @@ -82,6 +113,13 @@ export class ChatRouter extends RouterBroker { }, ) .post(this.routerPath('fetchProfilePictureUrl'), ...guards, async (req, res) => { + logger.verbose('request received in fetchProfilePictureUrl'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: profilePictureSchema, @@ -92,6 +130,13 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) .post(this.routerPath('findContacts'), ...guards, async (req, res) => { + logger.verbose('request received in findContacts'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: contactValidateSchema, @@ -102,6 +147,13 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) .post(this.routerPath('getBase64FromMediaMessage'), ...guards, async (req, res) => { + logger.verbose('request received in getBase64FromMediaMessage'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: null, @@ -113,6 +165,13 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) .post(this.routerPath('findMessages'), ...guards, async (req, res) => { + logger.verbose('request received in findMessages'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: messageValidateSchema, @@ -123,6 +182,13 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) .post(this.routerPath('findStatusMessage'), ...guards, async (req, res) => { + logger.verbose('request received in findStatusMessage'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: messageUpSchema, @@ -133,6 +199,13 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) .get(this.routerPath('findChats'), ...guards, async (req, res) => { + logger.verbose('request received in findChats'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: null, @@ -144,6 +217,13 @@ export class ChatRouter extends RouterBroker { }) // Profile routes .get(this.routerPath('fetchPrivacySettings'), ...guards, async (req, res) => { + logger.verbose('request received in fetchPrivacySettings'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: null, @@ -154,6 +234,13 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) .put(this.routerPath('updatePrivacySettings'), ...guards, async (req, res) => { + logger.verbose('request received in updatePrivacySettings'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: privacySettingsSchema, @@ -165,6 +252,13 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) .post(this.routerPath('fetchBusinessProfile'), ...guards, async (req, res) => { + logger.verbose('request received in fetchBusinessProfile'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: profilePictureSchema, @@ -176,6 +270,13 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) .post(this.routerPath('updateProfileName'), ...guards, async (req, res) => { + logger.verbose('request received in updateProfileName'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: profileNameSchema, @@ -186,6 +287,13 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) .post(this.routerPath('updateProfileStatus'), ...guards, async (req, res) => { + logger.verbose('request received in updateProfileStatus'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: profileStatusSchema, @@ -196,6 +304,13 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) .put(this.routerPath('updateProfilePicture'), ...guards, async (req, res) => { + logger.verbose('request received in updateProfilePicture'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: profilePictureSchema, @@ -207,6 +322,13 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) .delete(this.routerPath('removeProfilePicture'), ...guards, async (req, res) => { + logger.verbose('request received in removeProfilePicture'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: profilePictureSchema, diff --git a/src/whatsapp/routers/group.router.ts b/src/whatsapp/routers/group.router.ts index 17764f4e..8cb3032e 100644 --- a/src/whatsapp/routers/group.router.ts +++ b/src/whatsapp/routers/group.router.ts @@ -26,12 +26,21 @@ import { } from '../dto/group.dto'; import { groupController } from '../whatsapp.module'; import { HttpStatus } from './index.router'; +import { Logger } from '../../config/logger.config'; + +const logger = new Logger('GroupRouter'); export class GroupRouter extends RouterBroker { constructor(...guards: RequestHandler[]) { super(); this.router .post(this.routerPath('create'), ...guards, async (req, res) => { + logger.verbose('request received in createGroup'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: createGroupSchema, @@ -42,6 +51,13 @@ export class GroupRouter extends RouterBroker { res.status(HttpStatus.CREATED).json(response); }) .put(this.routerPath('updateGroupSubject'), ...guards, async (req, res) => { + logger.verbose('request received in updateGroupSubject'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ request: req, schema: updateGroupSubjectSchema, @@ -52,6 +68,12 @@ export class GroupRouter extends RouterBroker { res.status(HttpStatus.CREATED).json(response); }) .put(this.routerPath('updateGroupPicture'), ...guards, async (req, res) => { + logger.verbose('request received in updateGroupPicture'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.groupValidate({ request: req, schema: updateGroupPictureSchema, @@ -62,6 +84,12 @@ export class GroupRouter extends RouterBroker { res.status(HttpStatus.CREATED).json(response); }) .put(this.routerPath('updateGroupDescription'), ...guards, async (req, res) => { + logger.verbose('request received in updateGroupDescription'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.groupValidate({ request: req, schema: updateGroupDescriptionSchema, @@ -73,6 +101,12 @@ export class GroupRouter extends RouterBroker { res.status(HttpStatus.CREATED).json(response); }) .get(this.routerPath('findGroupInfos'), ...guards, async (req, res) => { + logger.verbose('request received in findGroupInfos'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.groupValidate({ request: req, schema: groupJidSchema, @@ -83,6 +117,12 @@ export class GroupRouter extends RouterBroker { res.status(HttpStatus.OK).json(response); }) .get(this.routerPath('fetchAllGroups'), ...guards, async (req, res) => { + logger.verbose('request received in fetchAllGroups'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.groupNoValidate({ request: req, schema: {}, @@ -93,6 +133,12 @@ export class GroupRouter extends RouterBroker { res.status(HttpStatus.OK).json(response); }) .get(this.routerPath('participants'), ...guards, async (req, res) => { + logger.verbose('request received in participants'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.groupValidate({ request: req, schema: groupJidSchema, @@ -103,6 +149,12 @@ export class GroupRouter extends RouterBroker { res.status(HttpStatus.OK).json(response); }) .get(this.routerPath('inviteCode'), ...guards, async (req, res) => { + logger.verbose('request received in inviteCode'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.groupValidate({ request: req, schema: groupJidSchema, @@ -113,6 +165,12 @@ export class GroupRouter extends RouterBroker { res.status(HttpStatus.OK).json(response); }) .get(this.routerPath('inviteInfo'), ...guards, async (req, res) => { + logger.verbose('request received in inviteInfo'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.inviteCodeValidate({ request: req, schema: groupInviteSchema, @@ -123,6 +181,12 @@ export class GroupRouter extends RouterBroker { res.status(HttpStatus.OK).json(response); }) .post(this.routerPath('sendInvite'), ...guards, async (req, res) => { + logger.verbose('request received in sendInvite'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.groupNoValidate({ request: req, schema: groupSendInviteSchema, @@ -133,6 +197,12 @@ export class GroupRouter extends RouterBroker { res.status(HttpStatus.OK).json(response); }) .put(this.routerPath('revokeInviteCode'), ...guards, async (req, res) => { + logger.verbose('request received in revokeInviteCode'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.groupValidate({ request: req, schema: groupJidSchema, @@ -143,6 +213,12 @@ export class GroupRouter extends RouterBroker { res.status(HttpStatus.CREATED).json(response); }) .put(this.routerPath('updateParticipant'), ...guards, async (req, res) => { + logger.verbose('request received in updateParticipant'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.groupValidate({ request: req, schema: updateParticipantsSchema, @@ -153,6 +229,12 @@ export class GroupRouter extends RouterBroker { res.status(HttpStatus.CREATED).json(response); }) .put(this.routerPath('updateSetting'), ...guards, async (req, res) => { + logger.verbose('request received in updateSetting'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.groupValidate({ request: req, schema: updateSettingsSchema, @@ -163,6 +245,12 @@ export class GroupRouter extends RouterBroker { res.status(HttpStatus.CREATED).json(response); }) .put(this.routerPath('toggleEphemeral'), ...guards, async (req, res) => { + logger.verbose('request received in toggleEphemeral'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.groupValidate({ request: req, schema: toggleEphemeralSchema, @@ -173,6 +261,12 @@ export class GroupRouter extends RouterBroker { res.status(HttpStatus.CREATED).json(response); }) .delete(this.routerPath('leaveGroup'), ...guards, async (req, res) => { + logger.verbose('request received in leaveGroup'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.groupValidate({ request: req, schema: {}, diff --git a/src/whatsapp/routers/instance.router.ts b/src/whatsapp/routers/instance.router.ts index 29a59a87..799c8249 100644 --- a/src/whatsapp/routers/instance.router.ts +++ b/src/whatsapp/routers/instance.router.ts @@ -7,6 +7,9 @@ import { HttpStatus } from './index.router'; import { OldToken } from '../services/auth.service'; import { Auth, ConfigService, Database } from '../../config/env.config'; import { dbserver } from '../../db/db.connect'; +import { Logger } from '../../config/logger.config'; + +const logger = new Logger('InstanceRouter'); export class InstanceRouter extends RouterBroker { constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) { @@ -14,6 +17,12 @@ export class InstanceRouter extends RouterBroker { const auth = configService.get('AUTHENTICATION'); this.router .post('/create', ...guards, async (req, res) => { + logger.verbose('request received in createInstance'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: instanceNameSchema, @@ -24,6 +33,12 @@ export class InstanceRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) .put(this.routerPath('restart'), ...guards, async (req, res) => { + logger.verbose('request received in restartInstance'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: instanceNameSchema, @@ -34,6 +49,12 @@ export class InstanceRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) .get(this.routerPath('connect'), ...guards, async (req, res) => { + logger.verbose('request received in connectInstance'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: instanceNameSchema, @@ -44,6 +65,12 @@ export class InstanceRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) .get(this.routerPath('connectionState'), ...guards, async (req, res) => { + logger.verbose('request received in connectionState'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: instanceNameSchema, @@ -54,6 +81,12 @@ export class InstanceRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) .get(this.routerPath('fetchInstances', false), ...guards, async (req, res) => { + logger.verbose('request received in fetchInstances'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: null, @@ -64,6 +97,12 @@ export class InstanceRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) .delete(this.routerPath('logout'), ...guards, async (req, res) => { + logger.verbose('request received in logoutInstances'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: instanceNameSchema, @@ -74,6 +113,12 @@ export class InstanceRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) .delete(this.routerPath('delete'), ...guards, async (req, res) => { + logger.verbose('request received in deleteInstances'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: instanceNameSchema, @@ -86,6 +131,12 @@ export class InstanceRouter extends RouterBroker { if (auth.TYPE === 'jwt') { this.router.put('/refreshToken', async (req, res) => { + logger.verbose('request received in refreshToken'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: oldTokenSchema, @@ -98,6 +149,12 @@ export class InstanceRouter extends RouterBroker { } this.router.delete('/deleteDatabase', async (req, res) => { + logger.verbose('request received in deleteDatabase'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const db = this.configService.get('DATABASE'); if (db.ENABLED) { try { diff --git a/src/whatsapp/routers/sendMessage.router.ts b/src/whatsapp/routers/sendMessage.router.ts index b2400f7f..d8d3acea 100644 --- a/src/whatsapp/routers/sendMessage.router.ts +++ b/src/whatsapp/routers/sendMessage.router.ts @@ -9,6 +9,7 @@ import { mediaMessageSchema, pollMessageSchema, reactionMessageSchema, + statusMessageSchema, stickerMessageSchema, textMessageSchema, } from '../../validate/validate.schema'; @@ -22,18 +23,28 @@ import { SendMediaDto, SendPollDto, SendReactionDto, + SendStatusDto, SendStickerDto, SendTextDto, } from '../dto/sendMessage.dto'; import { sendMessageController } from '../whatsapp.module'; import { RouterBroker } from '../abstract/abstract.router'; import { HttpStatus } from './index.router'; +import { Logger } from '../../config/logger.config'; + +const logger = new Logger('MessageRouter'); export class MessageRouter extends RouterBroker { constructor(...guards: RequestHandler[]) { super(); this.router .post(this.routerPath('sendText'), ...guards, async (req, res) => { + logger.verbose('request received in sendText'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: textMessageSchema, @@ -44,6 +55,12 @@ export class MessageRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) .post(this.routerPath('sendMedia'), ...guards, async (req, res) => { + logger.verbose('request received in sendMedia'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: mediaMessageSchema, @@ -54,6 +71,12 @@ export class MessageRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) .post(this.routerPath('sendWhatsAppAudio'), ...guards, async (req, res) => { + logger.verbose('request received in sendWhatsAppAudio'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: audioMessageSchema, @@ -65,6 +88,12 @@ export class MessageRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) .post(this.routerPath('sendButtons'), ...guards, async (req, res) => { + logger.verbose('request received in sendButtons'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: buttonMessageSchema, @@ -75,6 +104,12 @@ export class MessageRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) .post(this.routerPath('sendLocation'), ...guards, async (req, res) => { + logger.verbose('request received in sendLocation'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: locationMessageSchema, @@ -85,6 +120,12 @@ export class MessageRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) .post(this.routerPath('sendList'), ...guards, async (req, res) => { + logger.verbose('request received in sendList'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: listMessageSchema, @@ -95,6 +136,12 @@ export class MessageRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) .post(this.routerPath('sendContact'), ...guards, async (req, res) => { + logger.verbose('request received in sendContact'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: contactMessageSchema, @@ -105,6 +152,12 @@ export class MessageRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) .post(this.routerPath('sendReaction'), ...guards, async (req, res) => { + logger.verbose('request received in sendReaction'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: reactionMessageSchema, @@ -115,6 +168,12 @@ export class MessageRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) .post(this.routerPath('sendPoll'), ...guards, async (req, res) => { + logger.verbose('request received in sendPoll'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: pollMessageSchema, @@ -124,7 +183,29 @@ export class MessageRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) + .post(this.routerPath('sendStatus'), ...guards, async (req, res) => { + logger.verbose('request received in sendStatus'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: statusMessageSchema, + ClassRef: SendStatusDto, + execute: (instance, data) => sendMessageController.sendStatus(instance, data), + }); + + return res.status(HttpStatus.CREATED).json(response); + }) .post(this.routerPath('sendLinkPreview'), ...guards, async (req, res) => { + logger.verbose('request received in sendLinkPreview'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: linkPreviewSchema, @@ -136,6 +217,12 @@ export class MessageRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) .post(this.routerPath('sendSticker'), ...guards, async (req, res) => { + logger.verbose('request received in sendSticker'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: stickerMessageSchema, diff --git a/src/whatsapp/routers/webhook.router.ts b/src/whatsapp/routers/webhook.router.ts index 70a65a0c..c520d9d5 100644 --- a/src/whatsapp/routers/webhook.router.ts +++ b/src/whatsapp/routers/webhook.router.ts @@ -5,12 +5,21 @@ import { InstanceDto } from '../dto/instance.dto'; import { WebhookDto } from '../dto/webhook.dto'; import { webhookController } from '../whatsapp.module'; import { HttpStatus } from './index.router'; +import { Logger } from '../../config/logger.config'; + +const logger = new Logger('WebhookRouter'); export class WebhookRouter extends RouterBroker { constructor(...guards: RequestHandler[]) { super(); this.router .post(this.routerPath('set'), ...guards, async (req, res) => { + logger.verbose('request received in setWebhook'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: webhookSchema, @@ -21,6 +30,12 @@ export class WebhookRouter extends RouterBroker { res.status(HttpStatus.CREATED).json(response); }) .get(this.routerPath('find'), ...guards, async (req, res) => { + logger.verbose('request received in findWebhook'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: instanceNameSchema, diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index 73430e6f..fc261866 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -1,6 +1,6 @@ import { opendirSync, readdirSync, rmSync } from 'fs'; import { WAStartupService } from './whatsapp.service'; -import { INSTANCE_DIR } from '../../config/path.config'; +import { INSTANCE_DIR, STORE_DIR } from '../../config/path.config'; import EventEmitter2 from 'eventemitter2'; import { join } from 'path'; import { Logger } from '../../config/logger.config'; @@ -16,6 +16,7 @@ import { NotFoundException } from '../../exceptions'; import { Db } from 'mongodb'; import { initInstance } from '../whatsapp.module'; import { RedisCache } from '../../db/redis.client'; +import { execSync } from 'child_process'; export class WAMonitoringService { constructor( @@ -216,6 +217,24 @@ export class WAMonitoringService { rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true }); } + public async cleaningStoreFiles(instanceName: string) { + this.logger.verbose('cleaning store files instance: ' + instanceName); + + if (!this.db.ENABLED) { + const instance = this.waInstances[instanceName]; + + rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true }); + + execSync(`rm -rf ${join(STORE_DIR, 'chats', instanceName)}`); + execSync(`rm -rf ${join(STORE_DIR, 'contacts', instanceName)}`); + execSync(`rm -rf ${join(STORE_DIR, 'message-up', instanceName)}`); + execSync(`rm -rf ${join(STORE_DIR, 'messages', instanceName)}`); + + execSync(`rm -rf ${join(STORE_DIR, 'auth', 'apikey', instanceName + '.json')}`); + execSync(`rm -rf ${join(STORE_DIR, 'webhook', instanceName + '.json')}`); + } + } + public async loadInstance() { this.logger.verbose('load instances'); const set = async (name: string) => { @@ -300,6 +319,7 @@ export class WAMonitoringService { try { this.logger.verbose('request cleaning up instance: ' + instanceName); this.cleaningUp(instanceName); + this.cleaningStoreFiles(instanceName); } finally { this.logger.warn(`Instance "${instanceName}" - REMOVED`); } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 4f30f72c..f86c29cc 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -30,6 +30,7 @@ import makeWASocket, { WAMessageUpdate, WASocket, getAggregateVotesInPollMessage, + Browsers, } from '@whiskeysockets/baileys'; import { Auth, @@ -77,6 +78,8 @@ import { SendPollDto, SendLinkPreviewDto, SendStickerDto, + SendStatusDto, + StatusMessage, } from '../dto/sendMessage.dto'; import { arrayUnique, isBase64, isURL } from 'class-validator'; import { @@ -117,6 +120,7 @@ import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-st import sharp from 'sharp'; import { RedisCache } from '../../db/redis.client'; import { Log } from '../../config/env.config'; +import ProxyAgent from 'proxy-agent'; export class WAStartupService { constructor( @@ -505,7 +509,7 @@ export class WAStartupService { this.logger.verbose('Getting message with key: ' + JSON.stringify(key)); try { const webMessageInfo = (await this.repository.message.find({ - where: { owner: this.instance.wuid, key: { id: key.id } }, + where: { owner: this.instance.name, key: { id: key.id } }, })) as unknown as proto.IWebMessageInfo[]; if (full) { this.logger.verbose('Returning full message'); @@ -551,14 +555,14 @@ export class WAStartupService { `rm -rf ${join( this.storePath, key.toLowerCase().replace('_', '-'), - this.instance.wuid, + this.instance.name, )}/*.json`, ); this.logger.verbose( `Cleaned ${join( this.storePath, key.toLowerCase().replace('_', '-'), - this.instance.wuid, + this.instance.name, )}/*.json`, ); } @@ -598,7 +602,8 @@ export class WAStartupService { const { version } = await fetchLatestBaileysVersion(); this.logger.verbose('Baileys version: ' + version); const session = this.configService.get('CONFIG_SESSION_PHONE'); - const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; + // const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; + const browser: WABrowserDescription = Browsers.appropriate(session.CLIENT); this.logger.verbose('Browser: ' + JSON.stringify(browser)); const socketConfig: UserFacingSocketConfig = { @@ -672,7 +677,7 @@ export class WAStartupService { this.logger.verbose('Finding chats in database'); const chatsRepository = await this.repository.chat.find({ - where: { owner: this.instance.wuid }, + where: { owner: this.instance.name }, }); this.logger.verbose('Verifying if chats exists in database to insert'); @@ -689,7 +694,11 @@ export class WAStartupService { await this.sendDataWebhook(Events.CHATS_UPSERT, chatsRaw); this.logger.verbose('Inserting chats in database'); - await this.repository.chat.insert(chatsRaw, database.SAVE_DATA.CHATS); + await this.repository.chat.insert( + chatsRaw, + this.instance.name, + database.SAVE_DATA.CHATS, + ); }, 'chats.update': async ( @@ -717,7 +726,7 @@ export class WAStartupService { chats.forEach( async (chat) => await this.repository.chat.delete({ - where: { owner: this.instance.wuid, id: chat }, + where: { owner: this.instance.name, id: chat }, }), ); @@ -732,7 +741,7 @@ export class WAStartupService { this.logger.verbose('Finding contacts in database'); const contactsRepository = await this.repository.contact.find({ - where: { owner: this.instance.wuid }, + where: { owner: this.instance.name }, }); this.logger.verbose('Verifying if contacts exists in database to insert'); @@ -746,7 +755,7 @@ export class WAStartupService { id: contact.id, pushName: contact?.name || contact?.verifiedName, profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, - owner: this.instance.wuid, + owner: this.instance.name, }); } @@ -754,10 +763,14 @@ export class WAStartupService { await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw); this.logger.verbose('Inserting contacts in database'); - await this.repository.contact.insert(contactsRaw, database.SAVE_DATA.CONTACTS); + await this.repository.contact.insert( + contactsRaw, + this.instance.name, + database.SAVE_DATA.CONTACTS, + ); }, - 'contacts.update': async (contacts: Partial[]) => { + 'contacts.update': async (contacts: Partial[], database: Database) => { this.logger.verbose('Event received: contacts.update'); this.logger.verbose('Verifying if contacts exists in database to update'); @@ -767,12 +780,19 @@ export class WAStartupService { id: contact.id, pushName: contact?.name ?? contact?.verifiedName, profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, - owner: this.instance.wuid, + owner: this.instance.name, }); } this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); + + this.logger.verbose('Updating contacts in database'); + await this.repository.contact.update( + contactsRaw, + this.instance.name, + database.SAVE_DATA.CONTACTS, + ); }, }; @@ -796,7 +816,7 @@ export class WAStartupService { const chatsRaw: ChatRaw[] = chats.map((chat) => { return { id: chat.id, - owner: this.instance.wuid, + owner: this.instance.name, lastMsgTimestamp: chat.lastMessageRecvTimestamp, }; }); @@ -805,12 +825,16 @@ export class WAStartupService { await this.sendDataWebhook(Events.CHATS_SET, chatsRaw); this.logger.verbose('Inserting chats in database'); - await this.repository.chat.insert(chatsRaw, database.SAVE_DATA.CHATS); + await this.repository.chat.insert( + chatsRaw, + this.instance.name, + database.SAVE_DATA.CHATS, + ); } const messagesRaw: MessageRaw[] = []; const messagesRepository = await this.repository.message.find({ - where: { owner: this.instance.wuid }, + where: { owner: this.instance.name }, }); for await (const [, m] of Object.entries(messages)) { if (!m.message) { @@ -818,7 +842,7 @@ export class WAStartupService { } if ( messagesRepository.find( - (mr) => mr.owner === this.instance.wuid && mr.key.id === m.key.id, + (mr) => mr.owner === this.instance.name && mr.key.id === m.key.id, ) ) { continue; @@ -835,7 +859,7 @@ export class WAStartupService { message: { ...m.message }, messageType: getContentType(m.message), messageTimestamp: m.messageTimestamp as number, - owner: this.instance.wuid, + owner: this.instance.name, }); } @@ -877,7 +901,7 @@ export class WAStartupService { message: { ...received.message }, messageType: getContentType(received.message), messageTimestamp: received.messageTimestamp as number, - owner: this.instance.wuid, + owner: this.instance.name, source: getDevice(received.key.id), }; @@ -887,7 +911,63 @@ export class WAStartupService { await this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); this.logger.verbose('Inserting message in database'); - await this.repository.message.insert([messageRaw], database.SAVE_DATA.NEW_MESSAGE); + await this.repository.message.insert( + [messageRaw], + this.instance.name, + database.SAVE_DATA.NEW_MESSAGE, + ); + + this.logger.verbose('Verifying contact from message'); + const contact = await this.repository.contact.find({ + where: { owner: this.instance.name, id: received.key.remoteJid }, + }); + + const contactRaw: ContactRaw = { + id: received.key.remoteJid, + pushName: received.pushName, + profilePictureUrl: (await this.profilePicture(received.key.remoteJid)) + .profilePictureUrl, + owner: this.instance.name, + }; + + if (contactRaw.id === 'status@broadcast') { + this.logger.verbose('Contact is status@broadcast'); + return; + } + + if (contact?.length) { + this.logger.verbose('Contact found in database'); + const contactRaw: ContactRaw = { + id: received.key.remoteJid, + pushName: contact[0].pushName, + profilePictureUrl: (await this.profilePicture(received.key.remoteJid)) + .profilePictureUrl, + owner: this.instance.name, + }; + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); + await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); + + this.logger.verbose('Updating contact in database'); + await this.repository.contact.update( + [contactRaw], + this.instance.name, + database.SAVE_DATA.CONTACTS, + ); + return; + } + + this.logger.verbose('Contact not found in database'); + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); + await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); + + this.logger.verbose('Inserting contact in database'); + await this.repository.contact.insert( + [contactRaw], + this.instance.name, + database.SAVE_DATA.CONTACTS, + ); }, 'messages.update': async (args: WAMessageUpdate[], database: Database) => { @@ -921,7 +1001,7 @@ export class WAStartupService { ...key, status: status[update.status], datetime: Date.now(), - owner: this.instance.wuid, + owner: this.instance.name, pollUpdates, }; @@ -931,6 +1011,7 @@ export class WAStartupService { this.logger.verbose('Inserting message in database'); await this.repository.messageUpdate.insert( [message], + this.instance.name, database.SAVE_DATA.MESSAGE_UPDATE, ); } @@ -1050,7 +1131,7 @@ export class WAStartupService { if (events['contacts.update']) { this.logger.verbose('Listening event: contacts.update'); const payload = events['contacts.update']; - this.contactHandle['contacts.update'](payload); + this.contactHandle['contacts.update'](payload, database); } } }); @@ -1098,6 +1179,11 @@ export class WAStartupService { return number; } + if (number.includes('@broadcast')) { + this.logger.verbose('Number already contains @broadcast'); + return number; + } + const formattedBRNumber = this.formatBRNumber(number); if (formattedBRNumber !== number) { this.logger.verbose( @@ -1152,7 +1238,7 @@ export class WAStartupService { const jid = this.createJid(number); const isWA = (await this.whatsappNumber({ numbers: [jid] }))[0]; - if (!isWA.exists && !isJidGroup(isWA.jid)) { + if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { throw new BadRequestException(isWA); } @@ -1229,7 +1315,8 @@ export class WAStartupService { !message['audio'] && !message['poll'] && !message['linkPreview'] && - !message['sticker'] + !message['sticker'] && + !sender.includes('@broadcast') ) { if (!message['audio']) { this.logger.verbose('Sending message'); @@ -1258,6 +1345,20 @@ export class WAStartupService { ); } + if (sender.includes('@broadcast')) { + this.logger.verbose('Sending message'); + console.log(message['status']); + return await this.client.sendMessage( + sender, + message['status'].content as unknown as AnyMessageContent, + { + backgroundColor: message['status'].option.backgroundColor, + font: message['status'].option.font, + statusJidList: message['status'].option.statusJidList, + } as unknown as MiscMessageGenerationOptions, + ); + } + this.logger.verbose('Sending message'); return await this.client.sendMessage( sender, @@ -1335,6 +1436,131 @@ export class WAStartupService { ); } + private async formatStatusMessage(status: StatusMessage) { + this.logger.verbose('Formatting status message'); + + if (!status.type) { + throw new BadRequestException('Type is required'); + } + + if (!status.content) { + throw new BadRequestException('Content is required'); + } + + if (status.allContacts) { + this.logger.verbose('All contacts defined as true'); + + this.logger.verbose('Getting contacts from database'); + const contacts = await this.repository.contact.find({ + where: { owner: this.instance.name }, + }); + + if (!contacts.length) { + throw new BadRequestException('Contacts not found'); + } + + this.logger.verbose('Getting contacts with push name'); + status.statusJidList = contacts + .filter((contact) => contact.pushName) + .map((contact) => contact.id); + + this.logger.verbose(status.statusJidList); + } + + if (!status.statusJidList?.length && !status.allContacts) { + throw new BadRequestException('StatusJidList is required'); + } + + if (status.type === 'text') { + this.logger.verbose('Type defined as text'); + + if (!status.backgroundColor) { + throw new BadRequestException('Background color is required'); + } + + if (!status.font) { + throw new BadRequestException('Font is required'); + } + + return { + content: { + text: status.content, + }, + option: { + backgroundColor: status.backgroundColor, + font: status.font, + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'image') { + this.logger.verbose('Type defined as image'); + + return { + content: { + image: { + url: status.content, + }, + caption: status.caption, + }, + option: { + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'video') { + this.logger.verbose('Type defined as video'); + + return { + content: { + video: { + url: status.content, + }, + caption: status.caption, + }, + option: { + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'audio') { + this.logger.verbose('Type defined as audio'); + + this.logger.verbose('Processing audio'); + const convert = await this.processAudio(status.content, 'status@broadcast'); + if (typeof convert === 'string') { + this.logger.verbose('Audio processed'); + const audio = fs.readFileSync(convert).toString('base64'); + + const result = { + content: { + audio: Buffer.from(audio, 'base64'), + ptt: true, + mimetype: 'audio/mp4', + }, + option: { + statusJidList: status.statusJidList, + }, + }; + + fs.unlinkSync(convert); + + return result; + } else { + throw new InternalServerErrorException(convert); + } + } + + throw new BadRequestException('Type not found'); + } + + public async statusMessage(data: SendStatusDto) { + this.logger.verbose('Sending status message'); + return await this.sendMessageWithTyping('status@broadcast', { + status: await this.formatStatusMessage(data.statusMessage), + }); + } + private async prepareMediaMessage(mediaMessage: MediaMessage) { try { this.logger.verbose('Preparing media message'); @@ -1890,11 +2116,11 @@ export class WAStartupService { public async fetchContacts(query: ContactQuery) { this.logger.verbose('Fetching contacts'); if (query?.where) { - query.where.owner = this.instance.wuid; + query.where.owner = this.instance.name; } else { query = { where: { - owner: this.instance.wuid, + owner: this.instance.name, }, }; } @@ -1904,11 +2130,11 @@ export class WAStartupService { public async fetchMessages(query: MessageQuery) { this.logger.verbose('Fetching messages'); if (query?.where) { - query.where.owner = this.instance.wuid; + query.where.owner = this.instance.name; } else { query = { where: { - owner: this.instance.wuid, + owner: this.instance.name, }, limit: query?.limit, }; @@ -1919,11 +2145,11 @@ export class WAStartupService { public async fetchStatusMessage(query: MessageUpQuery) { this.logger.verbose('Fetching status messages'); if (query?.where) { - query.where.owner = this.instance.wuid; + query.where.owner = this.instance.name; } else { query = { where: { - owner: this.instance.wuid, + owner: this.instance.name, }, limit: query?.limit, }; @@ -1933,7 +2159,7 @@ export class WAStartupService { public async fetchChats() { this.logger.verbose('Fetching chats'); - return await this.repository.chat.find({ where: { owner: this.instance.wuid } }); + return await this.repository.chat.find({ where: { owner: this.instance.name } }); } public async fetchPrivacySettings() {