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 457863f3..9afa5dd0 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -93,6 +93,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 89d7c53e..5b80bb16 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -36,6 +36,7 @@ export type SaveData = { MESSAGE_UPDATE: boolean; CONTACTS: boolean; CHATS: boolean; + LABELS: boolean; }; export type StoreConf = { @@ -43,6 +44,7 @@ export type StoreConf = { MESSAGE_UP: boolean; CONTACTS: boolean; CHATS: boolean; + LABELS: boolean; }; export type CleanStoreConf = { @@ -106,6 +108,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; @@ -242,6 +246,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) @@ -265,6 +270,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: { @@ -331,6 +337,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 7eeb75c1..405487a1 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -136,6 +136,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..4a8f55ee 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,97 @@ 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 + add: + type: boolean + required: + - numberJid + - labelId + /settings/set/{instanceName}: post: tags: diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts index 65feab48..74a76724 100644 --- a/src/utils/i18n.ts +++ b/src/utils/i18n.ts @@ -4,8 +4,6 @@ import path from 'path'; import { ConfigService, Language } from '../config/env.config'; -// export class i18n { -// constructor(private readonly configService: ConfigService) { const languages = ['en', 'pt-BR']; const translationsPath = path.join(__dirname, 'translations'); const configService: ConfigService = new ConfigService(); @@ -31,6 +29,4 @@ i18next.init({ escapeValue: false, }, }); -// } -// } export default i18next; diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 7da554a0..f78b37f5 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', @@ -938,6 +940,8 @@ export const webhookSchema: JSONSchema7 = { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', @@ -1018,6 +1022,8 @@ export const websocketSchema: JSONSchema7 = { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', @@ -1061,6 +1067,8 @@ export const rabbitmqSchema: JSONSchema7 = { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', @@ -1104,6 +1112,8 @@ export const sqsSchema: JSONSchema7 = { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', @@ -1191,3 +1201,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/abstract/abstract.router.ts b/src/whatsapp/abstract/abstract.router.ts index 7c603880..18770ffa 100644 --- a/src/whatsapp/abstract/abstract.router.ts +++ b/src/whatsapp/abstract/abstract.router.ts @@ -21,7 +21,6 @@ const logger = new Logger('Validate'); export abstract class RouterBroker { constructor() {} public routerPath(path: string, param = true) { - // const route = param ? '/:instanceName/' + path : '/' + path; let route = '/' + path; param ? (route += '/:instanceName') : null; @@ -56,10 +55,6 @@ export abstract class RouterBroker { message = stack.replace('instance.', ''); } return message; - // return { - // property: property.replace('instance.', ''), - // message, - // }; }); logger.error(message); throw new BadRequestException(message); diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 016945e2..8f3286e4 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -155,6 +155,8 @@ export class InstanceController { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', @@ -205,6 +207,8 @@ export class InstanceController { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', @@ -252,6 +256,8 @@ export class InstanceController { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'LABELS_EDIT', + 'LABELS_ASSOCIATION', 'CALL', 'NEW_JWT_TOKEN', 'TYPEBOT_START', @@ -299,6 +305,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/settings.controller.ts b/src/whatsapp/controllers/settings.controller.ts index 0f559d1b..15563647 100644 --- a/src/whatsapp/controllers/settings.controller.ts +++ b/src/whatsapp/controllers/settings.controller.ts @@ -1,7 +1,4 @@ -// import { isURL } from 'class-validator'; - import { Logger } from '../../config/logger.config'; -// import { BadRequestException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { SettingsDto } from '../dto/settings.dto'; import { SettingsService } from '../services/settings.service'; 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 6f251b38..4ecc5ce0 100644 --- a/src/whatsapp/controllers/webhook.controller.ts +++ b/src/whatsapp/controllers/webhook.controller.ts @@ -54,6 +54,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..23ff47bb --- /dev/null +++ b/src/whatsapp/dto/label.dto.ts @@ -0,0 +1,12 @@ +export class LabelDto { + id?: string; + name: string; + color: number; + predefinedId?: string; +} + +export class HandleLabelDto { + number: string; + labelId: string; + action: 'add' | 'remove'; +} diff --git a/src/whatsapp/models/chat.model.ts b/src/whatsapp/models/chat.model.ts index 57a263fb..9e713a13 100644 --- a/src/whatsapp/models/chat.model.ts +++ b/src/whatsapp/models/chat.model.ts @@ -7,6 +7,7 @@ export class ChatRaw { id?: string; owner: string; lastMsgTimestamp?: number; + labels?: string[]; } type ChatRawBoolean = { @@ -18,6 +19,7 @@ const chatSchema = new Schema({ _id: { type: String, _id: true }, id: { type: String, required: true, minlength: 1 }, owner: { type: String, required: true, minlength: 1 }, + labels: { type: [String], default: [] }, }); export const ChatModel = dbserver?.model(ChatRaw.name, chatSchema, 'chats'); 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/auth.repository.ts b/src/whatsapp/repository/auth.repository.ts index 7aa1a427..fa520a16 100644 --- a/src/whatsapp/repository/auth.repository.ts +++ b/src/whatsapp/repository/auth.repository.ts @@ -1,4 +1,4 @@ -import { readFileSync } from 'fs'; +import { opendirSync, readFileSync } from 'fs'; import { join } from 'path'; import { Auth, ConfigService } from '../../config/env.config'; @@ -64,6 +64,37 @@ export class AuthRepository extends Repository { } } + public async list(): Promise { + try { + if (this.dbSettings.ENABLED) { + this.logger.verbose('listing auth in db'); + return await this.authModel.find(); + } + + this.logger.verbose('listing auth in store'); + + const auths: AuthRaw[] = []; + const openDir = opendirSync(join(AUTH_DIR, this.auth.TYPE), { + encoding: 'utf-8', + }); + for await (const dirent of openDir) { + if (dirent.isFile()) { + auths.push( + JSON.parse( + readFileSync(join(AUTH_DIR, this.auth.TYPE, dirent.name), { + encoding: 'utf-8', + }), + ), + ); + } + } + + return auths; + } catch (error) { + return []; + } + } + public async findInstanceNameById(instanceId: string): Promise { try { this.logger.verbose('finding auth by instanceId'); diff --git a/src/whatsapp/repository/chat.repository.ts b/src/whatsapp/repository/chat.repository.ts index 99cb61e8..5f2f50ce 100644 --- a/src/whatsapp/repository/chat.repository.ts +++ b/src/whatsapp/repository/chat.repository.ts @@ -115,4 +115,63 @@ export class ChatRepository extends Repository { return { error: error?.toString() }; } } + + public async update(data: ChatRaw[], instanceName: string, saveDb = false): Promise { + try { + this.logger.verbose('updating chats'); + + if (data.length === 0) { + this.logger.verbose('no chats to update'); + return; + } + + if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('updating chats in db'); + + const chats = data.map((chat) => { + return { + updateOne: { + filter: { id: chat.id }, + update: { ...chat }, + upsert: true, + }, + }; + }); + + const { nModified } = await this.chatModel.bulkWrite(chats); + + this.logger.verbose('chats updated in db: ' + nModified + ' chats'); + return { insertCount: nModified }; + } + + this.logger.verbose('updating chats in store'); + + const store = this.configService.get('STORE'); + + if (store.CONTACTS) { + this.logger.verbose('updating chats in store'); + data.forEach((chat) => { + this.writeStore({ + path: join(this.storePath, 'chats', instanceName), + fileName: chat.id, + data: chat, + }); + this.logger.verbose( + 'chats updated in store in path: ' + join(this.storePath, 'chats', instanceName) + '/' + chat.id, + ); + }); + + this.logger.verbose('chats updated in store: ' + data.length + ' chats'); + + return { insertCount: data.length }; + } + + this.logger.verbose('chats not updated'); + return { insertCount: 0 }; + } catch (error) { + return error; + } finally { + data = undefined; + } + } } diff --git a/src/whatsapp/repository/contact.repository.ts b/src/whatsapp/repository/contact.repository.ts index 074b12e9..d26ada35 100644 --- a/src/whatsapp/repository/contact.repository.ts +++ b/src/whatsapp/repository/contact.repository.ts @@ -11,6 +11,11 @@ export class ContactQuery { where: ContactRaw; } +export class ContactQueryMany { + owner: ContactRaw['owner']; + ids: ContactRaw['id'][]; +} + export class ContactRepository extends Repository { constructor(private readonly contactModel: IContactModel, private readonly configService: ConfigService) { super(configService); @@ -169,4 +174,54 @@ export class ContactRepository extends Repository { return []; } } + + public async findManyById(query: ContactQueryMany): Promise { + try { + this.logger.verbose('finding contacts'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding contacts in db'); + return await this.contactModel.find({ + owner: query.owner, + id: { $in: query.ids }, + }); + } + + this.logger.verbose('finding contacts in store'); + const contacts: ContactRaw[] = []; + if (query.ids.length > 0) { + this.logger.verbose('finding contacts in store by id'); + query.ids.forEach((id) => { + contacts.push( + JSON.parse( + readFileSync(join(this.storePath, 'contacts', query.owner, id + '.json'), { + encoding: 'utf-8', + }), + ), + ); + }); + } else { + this.logger.verbose('finding contacts in store by owner'); + + const openDir = opendirSync(join(this.storePath, 'contacts', query.owner), { + encoding: 'utf-8', + }); + for await (const dirent of openDir) { + if (dirent.isFile()) { + contacts.push( + JSON.parse( + readFileSync(join(this.storePath, 'contacts', query.owner, dirent.name), { + encoding: 'utf-8', + }), + ), + ); + } + } + } + + this.logger.verbose('contacts found in store: ' + contacts.length + ' contacts'); + return contacts; + } catch (error) { + return []; + } + } } 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/chat.router.ts b/src/whatsapp/routers/chat.router.ts index 6d0bc7f3..af6c6eca 100644 --- a/src/whatsapp/routers/chat.router.ts +++ b/src/whatsapp/routers/chat.router.ts @@ -62,7 +62,7 @@ export class ChatRouter extends RouterBroker { execute: (instance, data) => chatController.whatsappNumber(instance, data), }); - return res.status(HttpStatus.CREATED).json(response); + return res.status(HttpStatus.OK).json(response); }) .put(this.routerPath('markMessageAsRead'), ...guards, async (req, res) => { logger.verbose('request received in markMessageAsRead'); diff --git a/src/whatsapp/routers/chatwoot.router.ts b/src/whatsapp/routers/chatwoot.router.ts index eb779587..c232e007 100644 --- a/src/whatsapp/routers/chatwoot.router.ts +++ b/src/whatsapp/routers/chatwoot.router.ts @@ -5,7 +5,6 @@ import { chatwootSchema, instanceNameSchema } from '../../validate/validate.sche import { RouterBroker } from '../abstract/abstract.router'; import { ChatwootDto } from '../dto/chatwoot.dto'; import { InstanceDto } from '../dto/instance.dto'; -// import { ChatwootService } from '../services/chatwoot.service'; import { chatwootController } from '../whatsapp.module'; import { HttpStatus } from './index.router'; diff --git a/src/whatsapp/routers/index.router.ts b/src/whatsapp/routers/index.router.ts index 77214d46..51460339 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/routers/settings.router.ts b/src/whatsapp/routers/settings.router.ts index 6bd4d549..57e56b0d 100644 --- a/src/whatsapp/routers/settings.router.ts +++ b/src/whatsapp/routers/settings.router.ts @@ -5,7 +5,6 @@ import { instanceNameSchema, settingsSchema } from '../../validate/validate.sche import { RouterBroker } from '../abstract/abstract.router'; import { InstanceDto } from '../dto/instance.dto'; import { SettingsDto } from '../dto/settings.dto'; -// import { SettingsService } from '../services/settings.service'; import { settingsController } from '../whatsapp.module'; import { HttpStatus } from './index.router'; diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index cf511e1d..a38dd399 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -50,11 +50,6 @@ export class ChatwootService { this.cache.set(cacheKey, provider); return provider; - // try { - // } catch (error) { - // this.logger.error('provider not found'); - // return null; - // } } private async clientCw(instance: InstanceDto) { @@ -388,10 +383,6 @@ export class ChatwootService { q: query, }); } else { - // contact = await client.contacts.filter({ - // accountId: this.provider.account_id, - // payload: this.getFilterPayload(query), - // }); // hotfix for: https://github.com/EvolutionAPI/evolution-api/pull/382. waiting fix: https://github.com/figurolatam/chatwoot-sdk/pull/7 contact = await chatwootRequest(this.getClientCwConfig(), { method: 'POST', @@ -1193,7 +1184,6 @@ export class ChatwootService { if (state !== 'open') { if (state === 'close') { this.logger.verbose('request cleaning up instance: ' + instance.instanceName); - // await this.waMonitor.cleaningUp(instance.instanceName); } this.logger.verbose('connect to whatsapp'); const number = command.split(':')[1]; @@ -1204,6 +1194,12 @@ export class ChatwootService { } } + if (command === 'clearcache') { + this.logger.verbose('command clearcache found'); + waInstance.clearCacheChatwoot(); + await this.createBotMessage(instance, `✅ ${body.inbox.name} instance cache cleared.`, 'incoming'); + } + if (command === 'status') { this.logger.verbose('command status found'); @@ -2141,7 +2137,7 @@ export class ChatwootService { this.logger.verbose('qrcode success'); const fileData = Buffer.from(body?.qrcode.base64.replace('data:image/png;base64,', ''), 'base64'); - const fileName = `${path.join(waInstance?.storePath, 'temp', `${`${instance}.png`}`)}`; + const fileName = `${path.join(waInstance?.storePath, 'temp', `${instance.instanceName}.png`)}`; this.logger.verbose('temp file name: ' + fileName); diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index 7a0c1d2a..9e47bad6 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -2,22 +2,20 @@ import { execSync } from 'child_process'; import EventEmitter2 from 'eventemitter2'; import { opendirSync, readdirSync, rmSync, existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { Db } from 'mongodb'; +import { Collection } from 'mongoose'; import { join } from 'path'; import { Auth, ConfigService, Database, DelInstance, HttpServer, Redis } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; import { INSTANCE_DIR, STORE_DIR } from '../../config/path.config'; import { NotFoundException } from '../../exceptions'; -import { dbserver } from '../../libs/db.connect'; import { RedisCache } from '../../libs/redis.client'; import { AuthModel, ChamaaiModel, - // ChatModel, ChatwootModel, - // ContactModel, - // MessageModel, - // MessageUpModel, + ContactModel, + LabelModel, ProxyModel, RabbitmqModel, SettingsModel, @@ -42,7 +40,6 @@ export class WAMonitoringService { this.removeInstance(); this.noConnection(); - // this.delInstanceFiles(); Object.assign(this.db, configService.get('DATABASE')); Object.assign(this.redis, configService.get('REDIS')); @@ -57,8 +54,6 @@ export class WAMonitoringService { private dbInstance: Db; - private dbStore = dbserver; - private readonly logger = new Logger(WAMonitoringService.name); public readonly waInstances: Record = {}; @@ -321,17 +316,13 @@ 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; } this.logger.verbose('cleaning store database instance: ' + instanceName); - // await ChatModel.deleteMany({ owner: instanceName }); - // await ContactModel.deleteMany({ owner: instanceName }); - // await MessageUpModel.deleteMany({ owner: instanceName }); - // await MessageModel.deleteMany({ owner: instanceName }); - await AuthModel.deleteMany({ _id: instanceName }); await WebhookModel.deleteMany({ _id: instanceName }); await ChatwootModel.deleteMany({ _id: instanceName }); @@ -341,6 +332,8 @@ export class WAMonitoringService { await TypebotModel.deleteMany({ _id: instanceName }); await WebsocketModel.deleteMany({ _id: instanceName }); await SettingsModel.deleteMany({ _id: instanceName }); + await LabelModel.deleteMany({ owner: instanceName }); + await ContactModel.deleteMany({ owner: instanceName }); return; } @@ -395,7 +388,7 @@ export class WAMonitoringService { this.logger.verbose('Database enabled'); await this.repository.dbServer.connect(); const collections: any[] = await this.dbInstance.collections(); - + await this.deleteTempInstances(collections); if (collections.length > 0) { this.logger.verbose('Reading collections and setting instances'); await Promise.all(collections.map((coll) => this.setInstance(coll.namespace.replace(/^[\w-]+\./, '')))); @@ -507,4 +500,21 @@ export class WAMonitoringService { } } + private async deleteTempInstances(collections: Collection[]) { + this.logger.verbose('Cleaning up temp instances'); + const auths = await this.repository.auth.list(); + if (auths.length === 0) { + this.logger.verbose('No temp instances found'); + return; + } + let tempInstances = 0; + auths.forEach((auth) => { + if (collections.find((coll) => coll.namespace.replace(/^[\w-]+\./, '') === auth._id)) { + return; + } + tempInstances++; + this.eventEmitter.emit('remove.instance', auth._id, 'inner'); + }); + this.logger.verbose('Temp instances removed: ' + tempInstances); + } } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 8676cb49..58788e56 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1,3 +1,41 @@ +import ffmpegPath from '@ffmpeg-installer/ffmpeg'; +import { Boom } from '@hapi/boom'; +import makeWASocket, { + AnyMessageContent, + BufferedEventData, + BufferJSON, + CacheStore, + Chat, + ConnectionState, + Contact, + delay, + DisconnectReason, + downloadMediaMessage, + fetchLatestBaileysVersion, + generateWAMessageFromContent, + getAggregateVotesInPollMessage, + getContentType, + getDevice, + GroupMetadata, + isJidBroadcast, + isJidGroup, + isJidUser, + makeCacheableSignalKeyStore, + MessageUpsertType, + MiscMessageGenerationOptions, + ParticipantAction, + prepareWAMessageMedia, + proto, + useMultiFileAuthState, + UserFacingSocketConfig, + WABrowserDescription, + WAMediaUpload, + WAMessage, + 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 { execSync } from 'child_process'; import { isURL } from 'class-validator @@ -64,6 +102,7 @@ import { GroupUpdateSettingDto, } from '../dto/group.dto'; import { InstanceDto } from '../dto/instance.dto'; +import { HandleLabelDto, LabelDto } from '../dto/label.dto'; import { ContactMessage, MediaMessage, @@ -1452,7 +1491,7 @@ export class WAStartupService { ); this.logger.verbose('Verifying if contacts exists in database to insert'); - const contactsRaw: ContactRaw[] = []; + let contactsRaw: ContactRaw[] = []; for (const contact of contacts) { if (contactsRepository.has(contact.id)) { @@ -1462,7 +1501,7 @@ export class WAStartupService { contactsRaw.push({ id: contact.id, pushName: contact?.name || contact?.verifiedName || contact.id.split('@')[0], - profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, + profilePictureUrl: null, owner: this.instance.name, }); } @@ -1477,6 +1516,23 @@ export class WAStartupService { this.chatwootService.addHistoryContacts({ instanceName: this.instance.name }, contactsRaw); chatwootImport.importHistoryContacts({ instanceName: this.instance.name }, this.localChatwoot); } + + // Update profile pictures + contactsRaw = []; + for await (const contact of contacts) { + contactsRaw.push({ + id: contact.id, + pushName: contact?.name || contact?.verifiedName || contact.id.split('@')[0], + profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, + owner: this.instance.name, + }); + } + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); + this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw); + + this.logger.verbose('Updating contacts in database'); + this.repository.contact.update(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); } catch (error) { this.logger.error(error); } @@ -1942,10 +1998,86 @@ 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' }, + database: Database, + ) => { + this.logger.verbose('Sending data to webhook in event LABELS_ASSOCIATION'); + + // Atualiza labels nos chats + if (database.SAVE_DATA.CHATS) { + const chats = await this.repository.chat.find({ + where: { + owner: this.instance.name, + }, + }); + const chat = chats.find((c) => c.id === data.association.chatId); + if (chat) { + let labels = [...chat.labels]; + if (data.type === 'remove') { + labels = labels.filter((label) => label !== data.association.labelId); + } else if (data.type === 'add') { + labels = [...labels, data.association.labelId]; + } + await this.repository.chat.update( + [{ id: chat.id, owner: this.instance.name, labels }], + this.instance.name, + database.SAVE_DATA.CHATS, + ); + } + } + + // Envia dados para o webhook + this.sendDataWebhook(Events.LABELS_ASSOCIATION, { + instance: this.instance.name, + type: data.type, + chatId: data.association.chatId, + labelId: data.association.labelId, + }); + }, + }; + private eventHandler() { this.logger.verbose('Initializing event handler'); this.client.ev.process(async (events) => { if (!this.endSession) { + this.logger.verbose(`Event received: ${Object.keys(events).join(', ')}`); const database = this.configService.get('DATABASE'); const settings = await this.findSettings(); @@ -2078,6 +2210,20 @@ export class WAStartupService { 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, database); + 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; + } } }); } @@ -3107,6 +3253,10 @@ export class WAStartupService { onWhatsapp.push(...groups); // USERS + const contacts: ContactRaw[] = await this.repository.contact.findManyById({ + owner: this.instance.name, + ids: jids.users.map(({ jid }) => (jid.startsWith('+') ? jid.substring(1) : jid)), + }); const verify = await this.client.onWhatsApp( ...jids.users.map(({ jid }) => (!jid.startsWith('+') ? `+${jid}` : jid)), ); @@ -3116,18 +3266,6 @@ export class WAStartupService { const isBrWithDigit = user.jid.startsWith('55') && user.jid.slice(4, 5) === '9' && user.jid.length === 28; const jid = isBrWithDigit ? user.jid.slice(0, 4) + user.jid.slice(5) : user.jid; - const query: ContactQuery = { - where: { - owner: this.instance.name, - id: user.jid.startsWith('+') ? user.jid.substring(1) : user.jid, - }, - }; - const contacts: ContactRaw[] = await this.repository.contact.find(query); - let firstContactFound; - if (contacts.length > 0) { - firstContactFound = contacts[0].pushName; - } - const numberVerified = verify.find((v) => { const mainJidSimilarity = levenshtein.get(user.jid, v.jid) / Math.max(user.jid.length, v.jid.length); const jidSimilarity = levenshtein.get(jid, v.jid) / Math.max(jid.length, v.jid.length); @@ -3136,7 +3274,7 @@ export class WAStartupService { return { exists: !!numberVerified?.exists, jid: numberVerified?.jid || user.jid, - name: firstContactFound, + name: contacts.find((c) => c.id === jid)?.pushName, number: user.number, }; }), @@ -3808,4 +3946,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 8e82cb01..26e48505 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 6b0618e4..13d5484b 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'; @@ -82,6 +85,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, @@ -98,6 +102,7 @@ export const repository = new RepositoryBroker( proxyRepository, chamaaiRepository, authRepository, + labelRepository, configService, dbserver?.getClient(), ); @@ -165,6 +170,7 @@ 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); export const WAStartupClass: {