Merge branch 'develop' into main

This commit is contained in:
Davidson Gomes 2024-02-10 16:29:25 -03:00 committed by GitHub
commit f5d49c54e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 769 additions and 57 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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',

View File

@ -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

View File

@ -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:

View File

@ -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;

View File

@ -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'],
};

View File

@ -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);

View File

@ -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',

View File

@ -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);
}
}

View File

@ -38,6 +38,8 @@ export class RabbitmqController {
'GROUP_UPDATE',
'GROUP_PARTICIPANTS_UPDATE',
'CONNECTION_UPDATE',
'LABELS_EDIT',
'LABELS_ASSOCIATION',
'CALL',
'NEW_JWT_TOKEN',
'TYPEBOT_START',

View File

@ -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';

View File

@ -38,6 +38,8 @@ export class SqsController {
'GROUP_UPDATE',
'GROUP_PARTICIPANTS_UPDATE',
'CONNECTION_UPDATE',
'LABELS_EDIT',
'LABELS_ASSOCIATION',
'CALL',
'NEW_JWT_TOKEN',
'TYPEBOT_START',

View File

@ -54,6 +54,8 @@ export class WebhookController {
'GROUP_UPDATE',
'GROUP_PARTICIPANTS_UPDATE',
'CONNECTION_UPDATE',
'LABELS_EDIT',
'LABELS_ASSOCIATION',
'CALL',
'NEW_JWT_TOKEN',
'TYPEBOT_START',

View File

@ -38,6 +38,8 @@ export class WebsocketController {
'GROUP_UPDATE',
'GROUP_PARTICIPANTS_UPDATE',
'CONNECTION_UPDATE',
'LABELS_EDIT',
'LABELS_ASSOCIATION',
'CALL',
'NEW_JWT_TOKEN',
'TYPEBOT_START',

View File

@ -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';
}

View File

@ -7,6 +7,7 @@ export class ChatRaw {
id?: string;
owner: string;
lastMsgTimestamp?: number;
labels?: string[];
}
type ChatRawBoolean<T> = {
@ -18,6 +19,7 @@ const chatSchema = new Schema<ChatRaw>({
_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');

View File

@ -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';

View File

@ -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<T> = {
[P in keyof T]?: 0 | 1;
};
export type LabelRawSelect = LabelRawBoolean<LabelRaw>;
const labelSchema = new Schema<LabelRaw>({
_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;

View File

@ -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<AuthRaw[]> {
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<string | null> {
try {
this.logger.verbose('finding auth by instanceId');

View File

@ -115,4 +115,63 @@ export class ChatRepository extends Repository {
return { error: error?.toString() };
}
}
public async update(data: ChatRaw[], instanceName: string, saveDb = false): Promise<IInsert> {
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<StoreConf>('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;
}
}
}

View File

@ -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<ContactRaw[]> {
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 [];
}
}
}

View File

@ -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<LabelRaw>;
}
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<IInsert> {
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<StoreConf>('STORE');
if (store.LABELS) {
this.logger.verbose('saving label to store');
this.writeStore<LabelRaw>({
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<LabelRaw[]> {
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() };
}
}
}

View File

@ -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,
) {

View File

@ -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');

View File

@ -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';

View File

@ -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 };

View File

@ -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<LabelDto>({
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<HandleLabelDto>({
request: req,
schema: handleLabelSchema,
ClassRef: HandleLabelDto,
execute: (instance, data) => labelController.handleLabel(instance, data),
});
return res.status(HttpStatus.OK).json(response);
});
}
public readonly router = Router();
}

View File

@ -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';

View File

@ -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);

View File

@ -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>('DATABASE'));
Object.assign(this.redis, configService.get<Redis>('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<string, WAStartupService> = {};
@ -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<Document>[]) {
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);
}
}

View File

@ -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>('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<LabelDto[]> {
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());
}
}
}

View File

@ -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 {

View File

@ -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: {