From 23f1b4ac03d9eccb61262f6665f7186483ebc33a Mon Sep 17 00:00:00 2001 From: Judson Cairo Date: Thu, 8 Feb 2024 09:16:24 -0300 Subject: [PATCH] Implementation of Whatsapp labels management --- CHANGELOG.md | 1 + Docker/.env.example | 2 + .../evolution-api-all-services/.env.example | 2 + Dockerfile | 2 + src/config/env.config.ts | 8 ++ src/dev-env.yml | 2 + src/docs/swagger.yaml | 97 ++++++++++++++ src/validate/validate.schema.ts | 21 +++ .../controllers/instance.controller.ts | 8 ++ src/whatsapp/controllers/label.controller.ts | 20 +++ .../controllers/rabbitmq.controller.ts | 2 + src/whatsapp/controllers/sqs.controller.ts | 2 + .../controllers/webhook.controller.ts | 2 + .../controllers/websocket.controller.ts | 2 + src/whatsapp/dto/label.dto.ts | 121 ++++++++++++++++++ src/whatsapp/models/index.ts | 1 + src/whatsapp/models/label.model.ts | 29 +++++ src/whatsapp/repository/label.repository.ts | 111 ++++++++++++++++ src/whatsapp/repository/repository.manager.ts | 2 + src/whatsapp/routers/index.router.ts | 4 +- src/whatsapp/routers/label.router.ts | 53 ++++++++ src/whatsapp/services/monitor.service.ts | 3 + src/whatsapp/services/whatsapp.service.ts | 106 +++++++++++++++ src/whatsapp/types/wa.types.ts | 4 + src/whatsapp/whatsapp.module.ts | 6 + 25 files changed, 610 insertions(+), 1 deletion(-) create mode 100644 src/whatsapp/controllers/label.controller.ts create mode 100644 src/whatsapp/dto/label.dto.ts create mode 100644 src/whatsapp/models/label.model.ts create mode 100644 src/whatsapp/repository/label.repository.ts create mode 100644 src/whatsapp/routers/label.router.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a9284df3..bd553925 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Join in Group by Invite Code * Read messages from whatsapp in chatwoot * Add support to use use redis in cacheservice +* Add support for labels ### Fixed diff --git a/Docker/.env.example b/Docker/.env.example index 2813117e..dbc82634 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -84,6 +84,8 @@ WEBHOOK_EVENTS_GROUPS_UPSERT=true WEBHOOK_EVENTS_GROUPS_UPDATE=true WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=true WEBHOOK_EVENTS_CONNECTION_UPDATE=true +WEBHOOK_EVENTS_LABELS_EDIT=true +WEBHOOK_EVENTS_LABELS_ASSOCIATION=true WEBHOOK_EVENTS_CALL=true # This event fires every time a new token is requested via the refresh route WEBHOOK_EVENTS_NEW_JWT_TOKEN=false diff --git a/Docker/evolution-api-all-services/.env.example b/Docker/evolution-api-all-services/.env.example index 555ba7bc..ce71d917 100644 --- a/Docker/evolution-api-all-services/.env.example +++ b/Docker/evolution-api-all-services/.env.example @@ -73,6 +73,8 @@ WEBHOOK_EVENTS_GROUPS_UPSERT=true WEBHOOK_EVENTS_GROUPS_UPDATE=true WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=true WEBHOOK_EVENTS_CONNECTION_UPDATE=true +WEBHOOK_EVENTS_LABELS_EDIT=true +WEBHOOK_EVENTS_LABELS_ASSOCIATION=true # This event fires every time a new token is requested via the refresh route WEBHOOK_EVENTS_NEW_JWT_TOKEN=false diff --git a/Dockerfile b/Dockerfile index 9bc23317..201dda47 100644 --- a/Dockerfile +++ b/Dockerfile @@ -98,6 +98,8 @@ ENV WEBHOOK_EVENTS_GROUPS_UPSERT=true ENV WEBHOOK_EVENTS_GROUPS_UPDATE=true ENV WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=true ENV WEBHOOK_EVENTS_CONNECTION_UPDATE=true +ENV WEBHOOK_EVENTS_LABELS_EDIT=true +ENV WEBHOOK_EVENTS_LABELS_ASSOCIATION=true ENV WEBHOOK_EVENTS_CALL=true ENV WEBHOOK_EVENTS_NEW_JWT_TOKEN=false diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 22ea9b21..4c5c9120 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -34,6 +34,7 @@ export type SaveData = { MESSAGE_UPDATE: boolean; CONTACTS: boolean; CHATS: boolean; + LABELS: boolean; }; export type StoreConf = { @@ -41,6 +42,7 @@ export type StoreConf = { MESSAGE_UP: boolean; CONTACTS: boolean; CHATS: boolean; + LABELS: boolean; }; export type CleanStoreConf = { @@ -103,6 +105,8 @@ export type EventsWebhook = { CHATS_DELETE: boolean; CHATS_UPSERT: boolean; CONNECTION_UPDATE: boolean; + LABELS_EDIT: boolean; + LABELS_ASSOCIATION: boolean; GROUPS_UPSERT: boolean; GROUP_UPDATE: boolean; GROUP_PARTICIPANTS_UPDATE: boolean; @@ -237,6 +241,7 @@ export class ConfigService { MESSAGE_UP: process.env?.STORE_MESSAGE_UP === 'true', CONTACTS: process.env?.STORE_CONTACTS === 'true', CHATS: process.env?.STORE_CHATS === 'true', + LABELS: process.env?.STORE_LABELS === 'true', }, CLEAN_STORE: { CLEANING_INTERVAL: Number.isInteger(process.env?.CLEAN_STORE_CLEANING_TERMINAL) @@ -259,6 +264,7 @@ export class ConfigService { MESSAGE_UPDATE: process.env?.DATABASE_SAVE_MESSAGE_UPDATE === 'true', CONTACTS: process.env?.DATABASE_SAVE_DATA_CONTACTS === 'true', CHATS: process.env?.DATABASE_SAVE_DATA_CHATS === 'true', + LABELS: process.env?.DATABASE_SAVE_DATA_LABELS === 'true', }, }, REDIS: { @@ -323,6 +329,8 @@ export class ConfigService { CHATS_UPSERT: process.env?.WEBHOOK_EVENTS_CHATS_UPSERT === 'true', CHATS_DELETE: process.env?.WEBHOOK_EVENTS_CHATS_DELETE === 'true', CONNECTION_UPDATE: process.env?.WEBHOOK_EVENTS_CONNECTION_UPDATE === 'true', + LABELS_EDIT: process.env?.WEBHOOK_EVENTS_LABELS_EDIT === 'true', + LABELS_ASSOCIATION: process.env?.WEBHOOK_EVENTS_LABELS_ASSOCIATION === 'true', GROUPS_UPSERT: process.env?.WEBHOOK_EVENTS_GROUPS_UPSERT === 'true', GROUP_UPDATE: process.env?.WEBHOOK_EVENTS_GROUPS_UPDATE === 'true', GROUP_PARTICIPANTS_UPDATE: process.env?.WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true', diff --git a/src/dev-env.yml b/src/dev-env.yml index 07e71453..8567e117 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -127,6 +127,8 @@ WEBHOOK: GROUP_UPDATE: true GROUP_PARTICIPANTS_UPDATE: true CONNECTION_UPDATE: true + LABELS_EDIT: true + LABELS_ASSOCIATION: true CALL: true # This event fires every time a new token is requested via the refresh route NEW_JWT_TOKEN: false diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index b5b27c38..d18a5bf1 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -51,6 +51,7 @@ tags: - name: Send Message Controller - name: Chat Controller - name: Group Controller + - name: Label Controller - name: Profile Settings - name: JWT - name: Settings @@ -1856,6 +1857,8 @@ paths: "GROUP_UPDATE", "GROUP_PARTICIPANTS_UPDATE", "CONNECTION_UPDATE", + "LABELS_EDIT", + "LABELS_ASSOCIATION", "CALL", "NEW_JWT_TOKEN", ] @@ -1932,6 +1935,8 @@ paths: "GROUP_UPDATE", "GROUP_PARTICIPANTS_UPDATE", "CONNECTION_UPDATE", + "LABELS_EDIT", + "LABELS_ASSOCIATION", "CALL", "NEW_JWT_TOKEN", ] @@ -2008,6 +2013,8 @@ paths: "GROUP_UPDATE", "GROUP_PARTICIPANTS_UPDATE", "CONNECTION_UPDATE", + "LABELS_EDIT", + "LABELS_ASSOCIATION", "CALL", "NEW_JWT_TOKEN", ] @@ -2046,6 +2053,96 @@ paths: content: application/json: {} + /label/findLabels/{instanceName}: + get: + tags: + - Label Controller + summary: List all labels for an instance. + parameters: + - name: instanceName + in: path + schema: + type: string + required: true + description: "- required" + example: "evolution" + responses: + "200": + description: Successful response + content: + application/json: + schema: + type: array + items: + type: object + properties: + color: + type: integer + name: + type: string + id: + type: string + predefinedId: + type: string + required: + - color + - name + - id + /label/handleLabel/{instanceName}: + put: + tags: + - Label Controller + summary: Change the label (add or remove) for an specific chat. + parameters: + - name: instanceName + in: path + schema: + type: string + required: true + description: "- required" + example: "evolution" + requestBody: + content: + application/json: + schema: + type: object + properties: + number: + type: string + labelId: + type: string + action: + type: string + enum: + - add + - remove + required: + - number + - labelId + - action + example: + number: '553499999999' + labelId: '1' + action: add + responses: + "200": + description: Successful response + content: + application/json: + schema: + type: object + properties: + numberJid: + type: string + labelId: + type: string + remove: + type: boolean + required: + - numberJid + - labelId + - remove + /settings/set/{instanceName}: post: tags: diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 2dcdc206..3d9e1294 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -53,6 +53,8 @@ export const instanceNameSchema: JSONSchema7 = { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', @@ -897,6 +899,8 @@ export const webhookSchema: JSONSchema7 = { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', @@ -977,6 +981,8 @@ export const websocketSchema: JSONSchema7 = { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', @@ -1020,6 +1026,8 @@ export const rabbitmqSchema: JSONSchema7 = { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', @@ -1063,6 +1071,8 @@ export const sqsSchema: JSONSchema7 = { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', @@ -1150,3 +1160,14 @@ export const chamaaiSchema: JSONSchema7 = { required: ['enabled', 'url', 'token', 'waNumber', 'answerByAudio'], ...isNotEmpty('enabled', 'url', 'token', 'waNumber', 'answerByAudio'), }; + +export const handleLabelSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + labelId: { type: 'string' }, + action: { type: 'string', enum: ['add', 'remove'] }, + }, + required: ['number', 'labelId', 'action'], +}; diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 0e48c2bf..414a2c2d 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -151,6 +151,8 @@ export class InstanceController { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', @@ -201,6 +203,8 @@ export class InstanceController { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', @@ -248,6 +252,8 @@ export class InstanceController { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', @@ -295,6 +301,8 @@ export class InstanceController { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', diff --git a/src/whatsapp/controllers/label.controller.ts b/src/whatsapp/controllers/label.controller.ts new file mode 100644 index 00000000..6fd91d86 --- /dev/null +++ b/src/whatsapp/controllers/label.controller.ts @@ -0,0 +1,20 @@ +import { Logger } from '../../config/logger.config'; +import { InstanceDto } from '../dto/instance.dto'; +import { HandleLabelDto } from '../dto/label.dto'; +import { WAMonitoringService } from '../services/monitor.service'; + +const logger = new Logger('LabelController'); + +export class LabelController { + constructor(private readonly waMonitor: WAMonitoringService) {} + + public async fetchLabels({ instanceName }: InstanceDto) { + logger.verbose('requested fetchLabels from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].fetchLabels(); + } + + public async handleLabel({ instanceName }: InstanceDto, data: HandleLabelDto) { + logger.verbose('requested chat label change from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].handleLabel(data); + } +} diff --git a/src/whatsapp/controllers/rabbitmq.controller.ts b/src/whatsapp/controllers/rabbitmq.controller.ts index 8d33ce84..527c5006 100644 --- a/src/whatsapp/controllers/rabbitmq.controller.ts +++ b/src/whatsapp/controllers/rabbitmq.controller.ts @@ -38,6 +38,8 @@ export class RabbitmqController { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', diff --git a/src/whatsapp/controllers/sqs.controller.ts b/src/whatsapp/controllers/sqs.controller.ts index 063e29ed..8dfebc6c 100644 --- a/src/whatsapp/controllers/sqs.controller.ts +++ b/src/whatsapp/controllers/sqs.controller.ts @@ -38,6 +38,8 @@ export class SqsController { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', diff --git a/src/whatsapp/controllers/webhook.controller.ts b/src/whatsapp/controllers/webhook.controller.ts index 8201f1b5..0b591cc3 100644 --- a/src/whatsapp/controllers/webhook.controller.ts +++ b/src/whatsapp/controllers/webhook.controller.ts @@ -46,6 +46,8 @@ export class WebhookController { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', diff --git a/src/whatsapp/controllers/websocket.controller.ts b/src/whatsapp/controllers/websocket.controller.ts index 5771027a..15abde70 100644 --- a/src/whatsapp/controllers/websocket.controller.ts +++ b/src/whatsapp/controllers/websocket.controller.ts @@ -38,6 +38,8 @@ export class WebsocketController { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', diff --git a/src/whatsapp/dto/label.dto.ts b/src/whatsapp/dto/label.dto.ts new file mode 100644 index 00000000..ab7e564c --- /dev/null +++ b/src/whatsapp/dto/label.dto.ts @@ -0,0 +1,121 @@ +import { proto, WAPresence, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from '@whiskeysockets/baileys'; + +export class OnWhatsAppDto { + constructor( + public readonly jid: string, + public readonly exists: boolean, + public readonly number: string, + public readonly name?: string, + ) {} +} + +export class LabelDto { + id?: string; + name: string; + color: number; + predefinedId?: string; +} + +export class HandleLabelDto { + number: string; + labelId: string; + action: 'add' | 'remove'; +} + +export class WhatsAppNumberDto { + numbers: string[]; +} + +export class NumberDto { + number: string; +} + +export class NumberBusiness { + wid?: string; + jid?: string; + exists?: boolean; + isBusiness: boolean; + name?: string; + message?: string; + description?: string; + email?: string; + website?: string[]; + address?: string; +} + +export class ProfileNameDto { + name: string; +} + +export class ProfileStatusDto { + status: string; +} + +export class ProfilePictureDto { + number?: string; + // url or base64 + picture?: string; +} + +class Key { + id: string; + fromMe: boolean; + remoteJid: string; +} +export class ReadMessageDto { + read_messages: Key[]; +} + +export class LastMessage { + key: Key; + messageTimestamp?: number; +} + +export class ArchiveChatDto { + lastMessage?: LastMessage; + chat?: string; + archive: boolean; +} + +class PrivacySetting { + readreceipts: WAReadReceiptsValue; + profile: WAPrivacyValue; + status: WAPrivacyValue; + online: WAPrivacyOnlineValue; + last: WAPrivacyValue; + groupadd: WAPrivacyValue; +} + +export class PrivacySettingDto { + privacySettings: PrivacySetting; +} + +export class DeleteMessage { + id: string; + fromMe: boolean; + remoteJid: string; + participant?: string; +} +export class Options { + delay?: number; + presence?: WAPresence; +} +class OptionsMessage { + options: Options; +} +export class Metadata extends OptionsMessage { + number: string; +} + +export class SendPresenceDto extends Metadata { + options: { + presence: WAPresence; + delay: number; + }; +} + +export class UpdateMessageDto extends Metadata { + number: string; + key: proto.IMessageKey; + text: string; +} diff --git a/src/whatsapp/models/index.ts b/src/whatsapp/models/index.ts index 7903e5b5..4d21e9b8 100644 --- a/src/whatsapp/models/index.ts +++ b/src/whatsapp/models/index.ts @@ -3,6 +3,7 @@ export * from './chamaai.model'; export * from './chat.model'; export * from './chatwoot.model'; export * from './contact.model'; +export * from './label.model'; export * from './message.model'; export * from './proxy.model'; export * from './rabbitmq.model'; diff --git a/src/whatsapp/models/label.model.ts b/src/whatsapp/models/label.model.ts new file mode 100644 index 00000000..99c39909 --- /dev/null +++ b/src/whatsapp/models/label.model.ts @@ -0,0 +1,29 @@ +import { Schema } from 'mongoose'; + +import { dbserver } from '../../libs/db.connect'; + +export class LabelRaw { + _id?: string; + id?: string; + owner: string; + name: string; + color: number; + predefinedId?: string; +} + +type LabelRawBoolean = { + [P in keyof T]?: 0 | 1; +}; +export type LabelRawSelect = LabelRawBoolean; + +const labelSchema = new Schema({ + _id: { type: String, _id: true }, + id: { type: String, required: true, minlength: 1 }, + owner: { type: String, required: true, minlength: 1 }, + name: { type: String, required: true, minlength: 1 }, + color: { type: Number, required: true, min: 0, max: 19 }, + predefinedId: { type: String }, +}); + +export const LabelModel = dbserver?.model(LabelRaw.name, labelSchema, 'labels'); +export type ILabelModel = typeof LabelModel; diff --git a/src/whatsapp/repository/label.repository.ts b/src/whatsapp/repository/label.repository.ts new file mode 100644 index 00000000..7bbb0e58 --- /dev/null +++ b/src/whatsapp/repository/label.repository.ts @@ -0,0 +1,111 @@ +import { opendirSync, readFileSync, rmSync } from 'fs'; +import { join } from 'path'; + +import { ConfigService, StoreConf } from '../../config/env.config'; +import { Logger } from '../../config/logger.config'; +import { IInsert, Repository } from '../abstract/abstract.repository'; +import { ILabelModel, LabelRaw, LabelRawSelect } from '../models'; + +export class LabelQuery { + select?: LabelRawSelect; + where: Partial; +} + +export class LabelRepository extends Repository { + constructor(private readonly labelModel: ILabelModel, private readonly configService: ConfigService) { + super(configService); + } + + private readonly logger = new Logger('LabelRepository'); + + public async insert(data: LabelRaw, instanceName: string, saveDb = false): Promise { + this.logger.verbose('inserting labels'); + + try { + if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('saving labels to db'); + const insert = await this.labelModel.findOneAndUpdate({ id: data.id }, data, { upsert: true }); + + this.logger.verbose(`label ${data.name} saved to db`); + return { insertCount: Number(!!insert._id) }; + } + + this.logger.verbose('saving label to store'); + + const store = this.configService.get('STORE'); + + if (store.LABELS) { + this.logger.verbose('saving label to store'); + this.writeStore({ + path: join(this.storePath, 'labels', instanceName), + fileName: data.id, + data, + }); + this.logger.verbose( + 'labels saved to store in path: ' + join(this.storePath, 'labels', instanceName) + '/' + data.id, + ); + + this.logger.verbose(`label ${data.name} saved to store`); + return { insertCount: 1 }; + } + + this.logger.verbose('labels not saved to store'); + return { insertCount: 0 }; + } catch (error) { + return error; + } finally { + data = undefined; + } + } + + public async find(query: LabelQuery): Promise { + try { + this.logger.verbose('finding labels'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding labels in db'); + return await this.labelModel.find({ owner: query.where.owner }).select(query.select ?? {}); + } + + this.logger.verbose('finding labels in store'); + + const labels: LabelRaw[] = []; + const openDir = opendirSync(join(this.storePath, 'labels', query.where.owner)); + for await (const dirent of openDir) { + if (dirent.isFile()) { + labels.push( + JSON.parse( + readFileSync(join(this.storePath, 'labels', query.where.owner, dirent.name), { + encoding: 'utf-8', + }), + ), + ); + } + } + + this.logger.verbose('labels found in store: ' + labels.length + ' labels'); + return labels; + } catch (error) { + return []; + } + } + + public async delete(query: LabelQuery) { + try { + this.logger.verbose('deleting labels'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('deleting labels in db'); + return await this.labelModel.deleteOne({ ...query.where }); + } + + this.logger.verbose('deleting labels in store'); + rmSync(join(this.storePath, 'labels', query.where.owner, query.where.id + '.josn'), { + force: true, + recursive: true, + }); + + return { deleted: { labelId: query.where.id } }; + } catch (error) { + return { error: error?.toString() }; + } + } +} diff --git a/src/whatsapp/repository/repository.manager.ts b/src/whatsapp/repository/repository.manager.ts index ab4da1e3..57b63e16 100644 --- a/src/whatsapp/repository/repository.manager.ts +++ b/src/whatsapp/repository/repository.manager.ts @@ -9,6 +9,7 @@ import { ChamaaiRepository } from './chamaai.repository'; import { ChatRepository } from './chat.repository'; import { ChatwootRepository } from './chatwoot.repository'; import { ContactRepository } from './contact.repository'; +import { LabelRepository } from './label.repository'; import { MessageRepository } from './message.repository'; import { MessageUpRepository } from './messageUp.repository'; import { ProxyRepository } from './proxy.repository'; @@ -34,6 +35,7 @@ export class RepositoryBroker { public readonly proxy: ProxyRepository, public readonly chamaai: ChamaaiRepository, public readonly auth: AuthRepository, + public readonly labels: LabelRepository, private configService: ConfigService, dbServer?: MongoClient, ) { diff --git a/src/whatsapp/routers/index.router.ts b/src/whatsapp/routers/index.router.ts index 56b9301f..33cf94c9 100644 --- a/src/whatsapp/routers/index.router.ts +++ b/src/whatsapp/routers/index.router.ts @@ -9,6 +9,7 @@ import { ChatRouter } from './chat.router'; import { ChatwootRouter } from './chatwoot.router'; import { GroupRouter } from './group.router'; import { InstanceRouter } from './instance.router'; +import { LabelRouter } from './label.router'; import { ProxyRouter } from './proxy.router'; import { RabbitmqRouter } from './rabbitmq.router'; import { MessageRouter } from './sendMessage.router'; @@ -61,6 +62,7 @@ router .use('/sqs', new SqsRouter(...guards).router) .use('/typebot', new TypebotRouter(...guards).router) .use('/proxy', new ProxyRouter(...guards).router) - .use('/chamaai', new ChamaaiRouter(...guards).router); + .use('/chamaai', new ChamaaiRouter(...guards).router) + .use('/label', new LabelRouter(...guards).router); export { HttpStatus, router }; diff --git a/src/whatsapp/routers/label.router.ts b/src/whatsapp/routers/label.router.ts new file mode 100644 index 00000000..a856ff6d --- /dev/null +++ b/src/whatsapp/routers/label.router.ts @@ -0,0 +1,53 @@ +import { RequestHandler, Router } from 'express'; + +import { Logger } from '../../config/logger.config'; +import { handleLabelSchema } from '../../validate/validate.schema'; +import { RouterBroker } from '../abstract/abstract.router'; +import { HandleLabelDto, LabelDto } from '../dto/label.dto'; +import { labelController } from '../whatsapp.module'; +import { HttpStatus } from './index.router'; + +const logger = new Logger('LabelRouter'); + +export class LabelRouter extends RouterBroker { + constructor(...guards: RequestHandler[]) { + super(); + this.router + .get(this.routerPath('findLabels'), ...guards, async (req, res) => { + logger.verbose('request received in findLabels'); + 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, + ClassRef: LabelDto, + execute: (instance) => labelController.fetchLabels(instance), + }); + + return res.status(HttpStatus.OK).json(response); + }) + .put(this.routerPath('handleLabel'), ...guards, async (req, res) => { + logger.verbose('request received in handleLabel'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + + const response = await this.dataValidate({ + request: req, + schema: handleLabelSchema, + ClassRef: HandleLabelDto, + execute: (instance, data) => labelController.handleLabel(instance, data), + }); + + return res.status(HttpStatus.OK).json(response); + }); + } + + public readonly router = Router(); +} diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index 3c3e8881..2beec2fa 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -15,6 +15,7 @@ import { ChamaaiModel, // ChatModel, ChatwootModel, + LabelModel, // ContactModel, // MessageModel, // MessageUpModel, @@ -320,6 +321,7 @@ export class WAMonitoringService { execSync(`rm -rf ${join(STORE_DIR, 'typebot', instanceName + '*')}`); execSync(`rm -rf ${join(STORE_DIR, 'websocket', instanceName + '*')}`); execSync(`rm -rf ${join(STORE_DIR, 'settings', instanceName + '*')}`); + execSync(`rm -rf ${join(STORE_DIR, 'labels', instanceName + '*')}`); return; } @@ -340,6 +342,7 @@ export class WAMonitoringService { await TypebotModel.deleteMany({ _id: instanceName }); await WebsocketModel.deleteMany({ _id: instanceName }); await SettingsModel.deleteMany({ _id: instanceName }); + await LabelModel.deleteMany({ owner: instanceName }); return; } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 22f7e7b2..b2d3bccd 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -34,6 +34,8 @@ import makeWASocket, { WAMessageUpdate, WASocket, } from '@whiskeysockets/baileys'; +import { Label } from '@whiskeysockets/baileys/lib/Types/Label'; +import { LabelAssociation } from '@whiskeysockets/baileys/lib/Types/LabelAssociation'; import axios from 'axios'; import { exec, execSync } from 'child_process'; import { arrayUnique, isBase64, isURL } from 'class-validator'; @@ -105,6 +107,7 @@ import { GroupUpdateSettingDto, } from '../dto/group.dto'; import { InstanceDto } from '../dto/instance.dto'; +import { HandleLabelDto, LabelDto } from '../dto/label.dto'; import { ContactMessage, MediaMessage, @@ -2147,6 +2150,53 @@ export class WAStartupService { }, }; + private readonly labelHandle = { + [Events.LABELS_EDIT]: async (label: Label, database: Database) => { + this.logger.verbose('Event received: labels.edit'); + this.logger.verbose('Finding labels in database'); + const labelsRepository = await this.repository.labels.find({ + where: { owner: this.instance.name }, + }); + + const savedLabel = labelsRepository.find((l) => l.id === label.id); + if (label.deleted && savedLabel) { + this.logger.verbose('Sending data to webhook in event LABELS_EDIT'); + await this.repository.labels.delete({ + where: { owner: this.instance.name, id: label.id }, + }); + this.sendDataWebhook(Events.LABELS_EDIT, { ...label, instance: this.instance.name }); + return; + } + + const labelName = label.name.replace(/[^\x20-\x7E]/g, ''); + if (!savedLabel || savedLabel.color !== label.color || savedLabel.name !== labelName) { + this.logger.verbose('Sending data to webhook in event LABELS_EDIT'); + await this.repository.labels.insert( + { + color: label.color, + name: labelName, + owner: this.instance.name, + id: label.id, + predefinedId: label.predefinedId, + }, + this.instance.name, + database.SAVE_DATA.LABELS, + ); + this.sendDataWebhook(Events.LABELS_EDIT, { ...label, instance: this.instance.name }); + } + }, + + [Events.LABELS_ASSOCIATION]: async (data: { association: LabelAssociation; type: 'remove' | 'add' }) => { + this.logger.verbose('Sending data to webhook in event LABELS_ASSOCIATION'); + this.sendDataWebhook(Events.LABELS_ASSOCIATION, { + instance: this.instance.name, + type: data.type, + jid: data.association.chatId, + labelId: data.association.labelId, + }); + }, + }; + private eventHandler() { this.logger.verbose('Initializing event handler'); this.client.ev.process(async (events) => { @@ -2282,6 +2332,19 @@ export class WAStartupService { this.logger.verbose('Listening event: contacts.update'); const payload = events['contacts.update']; this.contactHandle['contacts.update'](payload, database); + + if (events[Events.LABELS_ASSOCIATION]) { + this.logger.verbose('Listening event: labels.association'); + const payload = events[Events.LABELS_ASSOCIATION]; + this.labelHandle[Events.LABELS_ASSOCIATION](payload); + return; + } + + if (events[Events.LABELS_EDIT]) { + this.logger.verbose('Listening event: labels.edit'); + const payload = events[Events.LABELS_EDIT]; + this.labelHandle[Events.LABELS_EDIT](payload, database); + return; } } }); @@ -4005,4 +4068,47 @@ export class WAStartupService { throw new BadRequestException('Unable to leave the group', error.toString()); } } + + public async fetchLabels(): Promise { + this.logger.verbose('Fetching labels'); + const labels = await this.repository.labels.find({ + where: { + owner: this.instance.name, + }, + }); + + return labels.map((label) => ({ + color: label.color, + name: label.name, + id: label.id, + predefinedId: label.predefinedId, + })); + } + + public async handleLabel(data: HandleLabelDto) { + this.logger.verbose('Adding label'); + const whatsappContact = await this.whatsappNumber({ numbers: [data.number] }); + if (whatsappContact.length === 0) { + throw new NotFoundException('Number not found'); + } + const contact = whatsappContact[0]; + if (!contact.exists) { + throw new NotFoundException('Number is not on WhatsApp'); + } + + try { + if (data.action === 'add') { + await this.client.addChatLabel(contact.jid, data.labelId); + + return { numberJid: contact.jid, labelId: data.labelId, add: true }; + } + if (data.action === 'remove') { + await this.client.removeChatLabel(contact.jid, data.labelId); + + return { numberJid: contact.jid, labelId: data.labelId, remove: true }; + } + } catch (error) { + throw new BadRequestException(`Unable to ${data.action} label to chat`, error.toString()); + } + } } diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index e2cfa195..21f5ee31 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -28,6 +28,10 @@ export enum Events { TYPEBOT_START = 'typebot.start', TYPEBOT_CHANGE_STATUS = 'typebot.change-status', CHAMA_AI_ACTION = 'chama-ai.action', + LABELS_EDIT = 'labels.edit', + LABELS_ASSOCIATION = 'labels.association', + CREDS_UPDATE = 'creds.update', + MESSAGING_HISTORY_SET = 'messaging-history.set', } export declare namespace wa { diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index 3e52504f..49beadfe 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -9,6 +9,7 @@ import { ChatController } from './controllers/chat.controller'; import { ChatwootController } from './controllers/chatwoot.controller'; import { GroupController } from './controllers/group.controller'; import { InstanceController } from './controllers/instance.controller'; +import { LabelController } from './controllers/label.controller'; import { ProxyController } from './controllers/proxy.controller'; import { RabbitmqController } from './controllers/rabbitmq.controller'; import { SendMessageController } from './controllers/sendMessage.controller'; @@ -33,11 +34,13 @@ import { WebhookModel, WebsocketModel, } from './models'; +import { LabelModel } from './models/label.model'; import { AuthRepository } from './repository/auth.repository'; import { ChamaaiRepository } from './repository/chamaai.repository'; import { ChatRepository } from './repository/chat.repository'; import { ChatwootRepository } from './repository/chatwoot.repository'; import { ContactRepository } from './repository/contact.repository'; +import { LabelRepository } from './repository/label.repository'; import { MessageRepository } from './repository/message.repository'; import { MessageUpRepository } from './repository/messageUp.repository'; import { ProxyRepository } from './repository/proxy.repository'; @@ -77,6 +80,7 @@ const sqsRepository = new SqsRepository(SqsModel, configService); const chatwootRepository = new ChatwootRepository(ChatwootModel, configService); const settingsRepository = new SettingsRepository(SettingsModel, configService); const authRepository = new AuthRepository(AuthModel, configService); +const labelRepository = new LabelRepository(LabelModel, configService); export const repository = new RepositoryBroker( messageRepository, @@ -93,6 +97,7 @@ export const repository = new RepositoryBroker( proxyRepository, chamaaiRepository, authRepository, + labelRepository, configService, dbserver?.getClient(), ); @@ -160,5 +165,6 @@ export const instanceController = new InstanceController( export const sendMessageController = new SendMessageController(waMonitor); export const chatController = new ChatController(waMonitor); export const groupController = new GroupController(waMonitor); +export const labelController = new LabelController(waMonitor); logger.info('Module - ON');