init project evolution api

This commit is contained in:
Davidson Gomes
2023-06-09 07:48:59 -03:00
commit 2a1c426311
90 changed files with 9820 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
import { existsSync, mkdirSync, writeFileSync } from 'fs';
import { join } from 'path';
import { ConfigService, Database } from '../../config/env.config';
import { ROOT_DIR } from '../../config/path.config';
export type IInsert = { insertCount: number };
export interface IRepository {
insert(data: any, saveDb?: boolean): Promise<IInsert>;
find(query: any): Promise<any>;
delete(query: any, force?: boolean): Promise<any>;
dbSettings: Database;
readonly storePath: string;
}
type WriteStore<U> = {
path: string;
fileName: string;
data: U;
};
export abstract class Repository implements IRepository {
constructor(configService: ConfigService) {
this.dbSettings = configService.get<Database>('DATABASE');
}
dbSettings: Database;
readonly storePath = join(ROOT_DIR, 'store');
public writeStore = <T = any>(create: WriteStore<T>) => {
if (!existsSync(create.path)) {
mkdirSync(create.path, { recursive: true });
}
try {
writeFileSync(
join(create.path, create.fileName + '.json'),
JSON.stringify({ ...create.data }),
{ encoding: 'utf-8' },
);
return { message: 'create - success' };
} finally {
create.data = undefined;
}
};
public insert(data: any, saveDb = false): Promise<IInsert> {
throw new Error('Method not implemented.');
}
public find(query: any): Promise<any> {
throw new Error('Method not implemented.');
}
delete(query: any, force?: boolean): Promise<any> {
throw new Error('Method not implemented.');
}
}

View File

@@ -0,0 +1,153 @@
import { InstanceDto } from '../dto/instance.dto';
import { JSONSchema7 } from 'json-schema';
import { Request } from 'express';
import { validate } from 'jsonschema';
import { BadRequestException } from '../../exceptions';
import 'express-async-errors';
import { Logger } from '../../config/logger.config';
import { GroupInvite, GroupJid } from '../dto/group.dto';
type DataValidate<T> = {
request: Request;
schema: JSONSchema7;
ClassRef: any;
execute: (instance: InstanceDto, data: T) => Promise<any>;
};
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;
return route;
}
public async dataValidate<T>(args: DataValidate<T>) {
const { request, schema, ClassRef, execute } = args;
const ref = new ClassRef();
const body = request.body;
const instance = request.params as unknown as InstanceDto;
if (request?.query && Object.keys(request.query).length > 0) {
Object.assign(instance, request.query);
}
if (request.originalUrl.includes('/instance/create')) {
Object.assign(instance, body);
}
Object.assign(ref, body);
const v = schema ? validate(ref, schema) : { valid: true, errors: [] };
if (!v.valid) {
const message: any[] = v.errors.map(({ property, stack, schema }) => {
let message: string;
if (schema['description']) {
message = schema['description'];
} else {
message = stack.replace('instance.', '');
}
return {
property: property.replace('instance.', ''),
message,
};
});
logger.error([...message]);
throw new BadRequestException(...message);
}
return await execute(instance, ref);
}
public async groupValidate<T>(args: DataValidate<T>) {
const { request, ClassRef, schema, execute } = args;
const groupJid = request.query as unknown as GroupJid;
if (!groupJid?.groupJid) {
throw new BadRequestException(
'The group id needs to be informed in the query',
'ex: "groupJid=120362@g.us"',
);
}
const instance = request.params as unknown as InstanceDto;
const body = request.body;
const ref = new ClassRef();
Object.assign(body, groupJid);
Object.assign(ref, body);
const v = validate(ref, schema);
if (!v.valid) {
const message: any[] = v.errors.map(({ property, stack, schema }) => {
let message: string;
if (schema['description']) {
message = schema['description'];
} else {
message = stack.replace('instance.', '');
}
return {
property: property.replace('instance.', ''),
message,
};
});
logger.error([...message]);
throw new BadRequestException(...message);
}
return await execute(instance, ref);
}
public async inviteCodeValidate<T>(args: DataValidate<T>) {
const { request, ClassRef, schema, execute } = args;
const inviteCode = request.query as unknown as GroupInvite;
if (!inviteCode?.inviteCode) {
throw new BadRequestException(
'The group invite code id needs to be informed in the query',
'ex: "inviteCode=F1EX5QZxO181L3TMVP31gY" (Obtained from group join link)',
);
}
const instance = request.params as unknown as InstanceDto;
const body = request.body;
const ref = new ClassRef();
Object.assign(body, inviteCode);
Object.assign(ref, body);
const v = validate(ref, schema);
console.log(v, '@checkei aqui');
if (!v.valid) {
const message: any[] = v.errors.map(({ property, stack, schema }) => {
let message: string;
if (schema['description']) {
message = schema['description'];
} else {
message = stack.replace('instance.', '');
}
return {
property: property.replace('instance.', ''),
message,
};
});
logger.error([...message]);
throw new BadRequestException(...message);
}
return await execute(instance, ref);
}
}

View File

@@ -0,0 +1,101 @@
import { proto } from '@evolution/base';
import {
ArchiveChatDto,
DeleteMessage,
NumberDto,
ProfileNameDto,
ProfilePictureDto,
ProfileStatusDto,
ReadMessageDto,
WhatsAppNumberDto,
} from '../dto/chat.dto';
import { InstanceDto } from '../dto/instance.dto';
import { ContactQuery } from '../repository/contact.repository';
import { MessageQuery } from '../repository/message.repository';
import { MessageUpQuery } from '../repository/messageUp.repository';
import { WAMonitoringService } from '../services/monitor.service';
export class ChatController {
constructor(private readonly waMonitor: WAMonitoringService) {}
public async whatsappNumber({ instanceName }: InstanceDto, data: WhatsAppNumberDto) {
return await this.waMonitor.waInstances[instanceName].whatsappNumber(data);
}
public async readMessage({ instanceName }: InstanceDto, data: ReadMessageDto) {
return await this.waMonitor.waInstances[instanceName].markMessageAsRead(data);
}
public async archiveChat({ instanceName }: InstanceDto, data: ArchiveChatDto) {
return await this.waMonitor.waInstances[instanceName].archiveChat(data);
}
public async deleteMessage({ instanceName }: InstanceDto, data: DeleteMessage) {
return await this.waMonitor.waInstances[instanceName].deleteMessage(data);
}
public async fetchProfilePicture({ instanceName }: InstanceDto, data: NumberDto) {
return await this.waMonitor.waInstances[instanceName].profilePicture(data.number);
}
public async fetchContacts({ instanceName }: InstanceDto, query: ContactQuery) {
return await this.waMonitor.waInstances[instanceName].fetchContacts(query);
}
public async getBase64FromMediaMessage(
{ instanceName }: InstanceDto,
message: proto.IWebMessageInfo,
) {
return await this.waMonitor.waInstances[instanceName].getBase64FromMediaMessage(
message,
);
}
public async fetchMessages({ instanceName }: InstanceDto, query: MessageQuery) {
return await this.waMonitor.waInstances[instanceName].fetchMessages(query);
}
public async fetchStatusMessage({ instanceName }: InstanceDto, query: MessageUpQuery) {
return await this.waMonitor.waInstances[instanceName].fetchStatusMessage(query);
}
public async fetchChats({ instanceName }: InstanceDto) {
return await this.waMonitor.waInstances[instanceName].fetchChats();
}
public async getBusinessProfile(
{ instanceName }: InstanceDto,
data: ProfilePictureDto,
) {
return await this.waMonitor.waInstances[instanceName].getBusinessProfile(data.number);
}
public async updateProfileName({ instanceName }: InstanceDto, data: ProfileNameDto) {
return await this.waMonitor.waInstances[instanceName].updateProfileName(data.name);
}
public async updateProfileStatus(
{ instanceName }: InstanceDto,
data: ProfileStatusDto,
) {
return await this.waMonitor.waInstances[instanceName].updateProfileStatus(
data.status,
);
}
public async updateProfilePicture(
{ instanceName }: InstanceDto,
data: ProfilePictureDto,
) {
return await this.waMonitor.waInstances[instanceName].updateProfilePicture(
data.picture,
);
}
public async removeProfilePicture(
{ instanceName }: InstanceDto,
data: ProfilePictureDto,
) {
return await this.waMonitor.waInstances[instanceName].removeProfilePicture();
}
}

View File

@@ -0,0 +1,72 @@
import {
CreateGroupDto,
GroupInvite,
GroupJid,
GroupPictureDto,
GroupToggleEphemeralDto,
GroupUpdateParticipantDto,
GroupUpdateSettingDto,
} from '../dto/group.dto';
import { InstanceDto } from '../dto/instance.dto';
import { WAMonitoringService } from '../services/monitor.service';
export class GroupController {
constructor(private readonly waMonitor: WAMonitoringService) {}
public async createGroup(instance: InstanceDto, create: CreateGroupDto) {
return await this.waMonitor.waInstances[instance.instanceName].createGroup(create);
}
public async updateGroupPicture(instance: InstanceDto, update: GroupPictureDto) {
return await this.waMonitor.waInstances[instance.instanceName].updateGroupPicture(
update,
);
}
public async findGroupInfo(instance: InstanceDto, groupJid: GroupJid) {
return await this.waMonitor.waInstances[instance.instanceName].findGroup(groupJid);
}
public async inviteCode(instance: InstanceDto, groupJid: GroupJid) {
return await this.waMonitor.waInstances[instance.instanceName].inviteCode(groupJid);
}
public async inviteInfo(instance: InstanceDto, inviteCode: GroupInvite) {
return await this.waMonitor.waInstances[instance.instanceName].inviteInfo(inviteCode);
}
public async revokeInviteCode(instance: InstanceDto, groupJid: GroupJid) {
return await this.waMonitor.waInstances[instance.instanceName].revokeInviteCode(
groupJid,
);
}
public async findParticipants(instance: InstanceDto, groupJid: GroupJid) {
return await this.waMonitor.waInstances[instance.instanceName].findParticipants(
groupJid,
);
}
public async updateGParticipate(
instance: InstanceDto,
update: GroupUpdateParticipantDto,
) {
return await this.waMonitor.waInstances[instance.instanceName].updateGParticipant(
update,
);
}
public async updateGSetting(instance: InstanceDto, update: GroupUpdateSettingDto) {
return await this.waMonitor.waInstances[instance.instanceName].updateGSetting(update);
}
public async toggleEphemeral(instance: InstanceDto, update: GroupToggleEphemeralDto) {
return await this.waMonitor.waInstances[instance.instanceName].toggleEphemeral(
update,
);
}
public async leaveGroup(instance: InstanceDto, groupJid: GroupJid) {
return await this.waMonitor.waInstances[instance.instanceName].leaveGroup(groupJid);
}
}

View File

@@ -0,0 +1,166 @@
import { delay } from '@evolution/base';
import EventEmitter2 from 'eventemitter2';
import { Auth, ConfigService } from '../../config/env.config';
import { BadRequestException, InternalServerErrorException } from '../../exceptions';
import { InstanceDto } from '../dto/instance.dto';
import { RepositoryBroker } from '../repository/repository.manager';
import { AuthService, OldToken } from '../services/auth.service';
import { WAMonitoringService } from '../services/monitor.service';
import { WAStartupService } from '../services/whatsapp.service';
import { WebhookService } from '../services/webhook.service';
import { Logger } from '../../config/logger.config';
export class InstanceController {
constructor(
private readonly waMonitor: WAMonitoringService,
private readonly configService: ConfigService,
private readonly repository: RepositoryBroker,
private readonly eventEmitter: EventEmitter2,
private readonly authService: AuthService,
private readonly webhookService: WebhookService,
) {}
private readonly logger = new Logger(InstanceController.name);
public async createInstance({ instanceName, webhook }: InstanceDto) {
//verifica se modo da instancia é container
const mode = this.configService.get<Auth>('AUTHENTICATION').INSTANCE.MODE;
if (mode === 'container') {
//verifica se ja existe uma instancia criada com qualquer nome
if (Object.keys(this.waMonitor.waInstances).length > 0) {
throw new BadRequestException([
'Instance already created',
'Only one instance can be created',
]);
}
const instance = new WAStartupService(
this.configService,
this.eventEmitter,
this.repository,
);
instance.instanceName = instanceName;
this.waMonitor.waInstances[instance.instanceName] = instance;
this.waMonitor.delInstanceTime(instance.instanceName);
const hash = await this.authService.generateHash({
instanceName: instance.instanceName,
});
if (webhook) {
try {
this.webhookService.create(instance, { enabled: true, url: webhook });
} catch (error) {
this.logger.log(error);
}
}
return {
instance: {
instanceName: instance.instanceName,
status: 'created',
},
hash,
webhook,
};
} else {
const instance = new WAStartupService(
this.configService,
this.eventEmitter,
this.repository,
);
instance.instanceName = instanceName;
this.waMonitor.waInstances[instance.instanceName] = instance;
this.waMonitor.delInstanceTime(instance.instanceName);
const hash = await this.authService.generateHash({
instanceName: instance.instanceName,
});
if (webhook) {
try {
this.webhookService.create(instance, { enabled: true, url: webhook });
} catch (error) {
this.logger.log(error);
}
}
return {
instance: {
instanceName: instance.instanceName,
status: 'created',
},
hash,
webhook,
};
}
}
public async connectToWhatsapp({ instanceName }: InstanceDto) {
try {
const instance = this.waMonitor.waInstances[instanceName];
const state = instance.connectionStatus?.state;
switch (state) {
case 'close':
await instance.connectToWhatsapp();
await delay(2000);
return instance.qrCode;
case 'connecting':
return instance.qrCode;
default:
return await this.connectionState({ instanceName });
}
} catch (error) {
this.logger.log(error);
}
}
public async connectionState({ instanceName }: InstanceDto) {
return this.waMonitor.waInstances[instanceName].connectionStatus;
}
public async fetchInstances({ instanceName }: InstanceDto) {
if (instanceName) {
return this.waMonitor.instanceInfo(instanceName);
}
return this.waMonitor.instanceInfo();
}
public async logout({ instanceName }: InstanceDto) {
try {
await this.waMonitor.waInstances[instanceName]?.client?.logout(
'Log out instance: ' + instanceName,
);
this.waMonitor.waInstances[instanceName]?.client?.ws?.close();
this.waMonitor.waInstances[instanceName]?.client?.end(undefined);
return { error: false, message: 'Instance logged out' };
} catch (error) {
throw new InternalServerErrorException(error.toString());
}
}
public async deleteInstance({ instanceName }: InstanceDto) {
const stateConn = await this.connectionState({ instanceName });
if (stateConn.state === 'open') {
throw new BadRequestException([
'Deletion failed',
'The instance needs to be disconnected',
]);
}
try {
delete this.waMonitor.waInstances[instanceName];
return { error: false, message: 'Instance deleted' };
} catch (error) {
throw new BadRequestException(error.toString());
}
}
public async refreshToken(_: InstanceDto, oldToken: OldToken) {
return await this.authService.refreshToken(oldToken);
}
}

View File

@@ -0,0 +1,78 @@
import { isBase64, isURL } from 'class-validator';
import { BadRequestException } from '../../exceptions';
import { InstanceDto } from '../dto/instance.dto';
import {
SendAudioDto,
SendButtonDto,
SendContactDto,
SendLinkPreviewDto,
SendListDto,
SendLocationDto,
SendMediaDto,
SendPollDto,
SendReactionDto,
SendTextDto,
} from '../dto/sendMessage.dto';
import { WAMonitoringService } from '../services/monitor.service';
export class SendMessageController {
constructor(private readonly waMonitor: WAMonitoringService) {}
public async sendText({ instanceName }: InstanceDto, data: SendTextDto) {
return await this.waMonitor.waInstances[instanceName].textMessage(data);
}
public async sendMedia({ instanceName }: InstanceDto, data: SendMediaDto) {
if (isBase64(data?.mediaMessage?.media) && !data?.mediaMessage?.fileName) {
throw new BadRequestException('For bse64 the file name must be informed.');
}
if (isURL(data?.mediaMessage?.media) || isBase64(data?.mediaMessage?.media)) {
return await this.waMonitor.waInstances[instanceName].mediaMessage(data);
}
throw new BadRequestException('Owned media must be a url or base64');
}
public async sendWhatsAppAudio({ instanceName }: InstanceDto, data: SendAudioDto) {
if (isURL(data.audioMessage.audio) || isBase64(data.audioMessage.audio)) {
return await this.waMonitor.waInstances[instanceName].audioWhatsapp(data);
}
throw new BadRequestException('Owned media must be a url or base64');
}
public async sendButtons({ instanceName }: InstanceDto, data: SendButtonDto) {
if (
isBase64(data.buttonMessage.mediaMessage?.media) &&
!data.buttonMessage.mediaMessage?.fileName
) {
throw new BadRequestException('For bse64 the file name must be informed.');
}
return await this.waMonitor.waInstances[instanceName].buttonMessage(data);
}
public async sendLocation({ instanceName }: InstanceDto, data: SendLocationDto) {
return await this.waMonitor.waInstances[instanceName].locationMessage(data);
}
public async sendList({ instanceName }: InstanceDto, data: SendListDto) {
return await this.waMonitor.waInstances[instanceName].listMessage(data);
}
public async sendContact({ instanceName }: InstanceDto, data: SendContactDto) {
return await this.waMonitor.waInstances[instanceName].contactMessage(data);
}
public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) {
if (!data.reactionMessage.reaction.match(/[^\(\)\w\sà-ú"-\+]+/)) {
throw new BadRequestException('"reaction" must be an emoji');
}
return await this.waMonitor.waInstances[instanceName].reactionMessage(data);
}
public async sendPoll({ instanceName }: InstanceDto, data: SendPollDto) {
return await this.waMonitor.waInstances[instanceName].pollMessage(data);
}
public async sendLinkPreview({ instanceName }: InstanceDto, data: SendLinkPreviewDto) {
return await this.waMonitor.waInstances[instanceName].linkPreview(data);
}
}

View File

@@ -0,0 +1,28 @@
import { Request, Response } from 'express';
import { Auth, ConfigService } from '../../config/env.config';
import { BadRequestException } from '../../exceptions';
import { InstanceDto } from '../dto/instance.dto';
import { HttpStatus } from '../routers/index.router';
import { WAMonitoringService } from '../services/monitor.service';
export class ViewsController {
constructor(
private readonly waMonit: WAMonitoringService,
private readonly configService: ConfigService,
) {}
public async qrcode(request: Request, response: Response) {
try {
const param = request.params as unknown as InstanceDto;
const instance = this.waMonit.waInstances[param.instanceName];
if (instance.connectionStatus.state === 'open') {
throw new BadRequestException('The instance is already connected');
}
const type = this.configService.get<Auth>('AUTHENTICATION').TYPE;
return response.status(HttpStatus.OK).render('qrcode', { type, ...param });
} catch (error) {
console.log('ERROR: ', error);
}
}
}

View File

@@ -0,0 +1,20 @@
import { isURL } from 'class-validator';
import { BadRequestException } from '../../exceptions';
import { InstanceDto } from '../dto/instance.dto';
import { WebhookDto } from '../dto/webhook.dto';
import { WebhookService } from '../services/webhook.service';
export class WebhookController {
constructor(private readonly webhookService: WebhookService) {}
public async createWebhook(instance: InstanceDto, data: WebhookDto) {
if (!isURL(data.url, { require_tld: false })) {
throw new BadRequestException('Invalid "url" property');
}
return this.webhookService.create(instance, data);
}
public async findWebhook(instance: InstanceDto) {
return this.webhookService.find(instance);
}
}

View File

@@ -0,0 +1,55 @@
export class OnWhatsAppDto {
constructor(
public readonly jid: string,
public readonly exists: boolean,
public readonly name?: string,
) {}
}
export class WhatsAppNumberDto {
numbers: string[];
}
export class NumberDto {
number: 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 {
readMessages: Key[];
}
class LastMessage {
key: Key;
messageTimestamp?: number;
}
export class ArchiveChatDto {
lastMessage: LastMessage;
archive: boolean;
}
export class DeleteMessage {
id: string;
fromMe: boolean;
remoteJid: string;
participant?: string;
}

View File

@@ -0,0 +1,31 @@
export class CreateGroupDto {
subject: string;
description?: string;
participants: string[];
}
export class GroupPictureDto {
groupJid: string;
image: string;
}
export class GroupJid {
groupJid: string;
}
export class GroupInvite {
inviteCode: string;
}
export class GroupUpdateParticipantDto extends GroupJid {
action: 'add' | 'remove' | 'promote' | 'demote';
participants: string[];
}
export class GroupUpdateSettingDto extends GroupJid {
action: 'announcement' | 'not_announcement' | 'unlocked' | 'locked';
}
export class GroupToggleEphemeralDto extends GroupJid {
expiration: 0 | 86400 | 604800 | 7776000;
}

View File

@@ -0,0 +1,4 @@
export class InstanceDto {
instanceName: string;
webhook?: string;
}

View File

@@ -0,0 +1,133 @@
import { proto, WAPresence } from '@evolution/base';
export class Quoted {
key: proto.IMessageKey;
message: proto.IMessage;
}
export class Mentions {
everyOne: boolean;
mentioned: string[];
}
export class Options {
delay?: number;
presence?: WAPresence;
quoted?: Quoted;
mentions?: Mentions;
}
class OptionsMessage {
options: Options;
}
export class Metadata extends OptionsMessage {
number: string;
}
class TextMessage {
text: string;
}
class linkPreviewMessage {
text: string;
}
class PollMessage {
name: string;
selectableCount: number;
values: string[];
messageSecret?: Uint8Array;
}
export class SendTextDto extends Metadata {
textMessage: TextMessage;
}
export class SendLinkPreviewDto extends Metadata {
linkPreview: linkPreviewMessage;
}
export class SendPollDto extends Metadata {
pollMessage: PollMessage;
}
export type MediaType = 'image' | 'document' | 'video' | 'audio';
export class MediaMessage {
mediatype: MediaType;
caption?: string;
// for document
fileName?: string;
// url or base64
media: string;
}
export class SendMediaDto extends Metadata {
mediaMessage: MediaMessage;
}
class Audio {
audio: string;
}
export class SendAudioDto extends Metadata {
audioMessage: Audio;
}
class Button {
buttonText: string;
buttonId: string;
}
class ButtonMessage {
title: string;
description: string;
footerText?: string;
buttons: Button[];
mediaMessage?: MediaMessage;
}
export class SendButtonDto extends Metadata {
buttonMessage: ButtonMessage;
}
class LocationMessage {
latitude: number;
longitude: number;
name?: string;
address?: string;
}
export class SendLocationDto extends Metadata {
locationMessage: LocationMessage;
}
class Row {
title: string;
description: string;
rowId: string;
}
class Section {
title: string;
rows: Row[];
}
class ListMessage {
title: string;
description: string;
footerText?: string;
buttonText: string;
sections: Section[];
}
export class SendListDto extends Metadata {
listMessage: ListMessage;
}
export class ContactMessage {
fullName: string;
wuid: string;
phoneNumber: string;
}
export class SendContactDto extends Metadata {
contactMessage: ContactMessage[];
}
class ReactionMessage {
key: proto.IMessageKey;
reaction: string;
}
export class SendReactionDto {
reactionMessage: ReactionMessage;
}

View File

@@ -0,0 +1,4 @@
export class WebhookDto {
enabled?: boolean;
url?: string;
}

View File

@@ -0,0 +1,94 @@
import { isJWT } from 'class-validator';
import { NextFunction, Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import { Auth, configService } from '../../config/env.config';
import { Logger } from '../../config/logger.config';
import { name } from '../../../package.json';
import { InstanceDto } from '../dto/instance.dto';
import { JwtPayload } from '../services/auth.service';
import { ForbiddenException, UnauthorizedException } from '../../exceptions';
import { repository } from '../whatsapp.module';
const logger = new Logger('GUARD');
async function jwtGuard(req: Request, res: Response, next: NextFunction) {
const key = req.get('apikey');
if (key && configService.get<Auth>('AUTHENTICATION').API_KEY.KEY !== key) {
throw new UnauthorizedException();
}
if (configService.get<Auth>('AUTHENTICATION').API_KEY.KEY === key) {
return next();
}
if (
(req.originalUrl.includes('/instance/create') ||
req.originalUrl.includes('/instance/fetchInstances')) &&
!key
) {
throw new ForbiddenException(
'Missing global api key',
'The global api key must be set',
);
}
const jwtOpts = configService.get<Auth>('AUTHENTICATION').JWT;
try {
const [bearer, token] = req.get('authorization').split(' ');
if (bearer.toLowerCase() !== 'bearer') {
throw new UnauthorizedException();
}
if (!isJWT(token)) {
throw new UnauthorizedException();
}
const param = req.params as unknown as InstanceDto;
const decode = jwt.verify(token, jwtOpts.SECRET, {
ignoreExpiration: jwtOpts.EXPIRIN_IN === 0,
}) as JwtPayload;
if (param.instanceName !== decode.instanceName || name !== decode.apiName) {
throw new UnauthorizedException();
}
return next();
} catch (error) {
logger.error(error);
throw new UnauthorizedException();
}
}
async function apikey(req: Request, res: Response, next: NextFunction) {
const env = configService.get<Auth>('AUTHENTICATION').API_KEY;
const key = req.get('apikey');
if (env.KEY === key) {
return next();
}
if (
(req.originalUrl.includes('/instance/create') ||
req.originalUrl.includes('/instance/fetchInstances')) &&
!key
) {
throw new ForbiddenException(
'Missing global api key',
'The global api key must be set',
);
}
try {
const param = req.params as unknown as InstanceDto;
const instanceKey = await repository.auth.find(param.instanceName);
if (instanceKey.apikey === key) {
return next();
}
} catch (error) {}
throw new UnauthorizedException();
}
export const authGuard = { jwt: jwtGuard, apikey };

View File

@@ -0,0 +1,63 @@
import { NextFunction, Request, Response } from 'express';
import { existsSync } from 'fs';
import { join } from 'path';
import { INSTANCE_DIR } from '../../config/path.config';
import { db, dbserver } from '../../db/db.connect';
import {
BadRequestException,
ForbiddenException,
NotFoundException,
} from '../../exceptions';
import { InstanceDto } from '../dto/instance.dto';
import { waMonitor } from '../whatsapp.module';
async function getInstance(instanceName: string) {
const exists = waMonitor.waInstances[instanceName];
if (db.ENABLED) {
const collection = dbserver
.getClient()
.db(db.CONNECTION.DB_PREFIX_NAME + '-instances')
.collection(instanceName);
return exists || (await collection.find({}).toArray()).length > 0;
}
return exists || existsSync(join(INSTANCE_DIR, instanceName));
}
export async function instanceExistsGuard(req: Request, _: Response, next: NextFunction) {
if (
req.originalUrl.includes('/instance/create') ||
req.originalUrl.includes('/instance/fetchInstances')
) {
return next();
}
const param = req.params as unknown as InstanceDto;
if (!param?.instanceName) {
throw new BadRequestException('"instanceName" not provided.');
}
if (!(await getInstance(param.instanceName))) {
throw new NotFoundException(`The "${param.instanceName}" instance does not exist`);
}
next();
}
export async function instanceLoggedGuard(req: Request, _: Response, next: NextFunction) {
if (req.originalUrl.includes('/instance/create')) {
const instance = req.body as InstanceDto;
if (await getInstance(instance.instanceName)) {
throw new ForbiddenException(
`This name "${instance.instanceName}" is already in use.`,
);
}
if (waMonitor.waInstances[instance.instanceName]) {
delete waMonitor.waInstances[instance.instanceName];
}
}
next();
}

View File

@@ -0,0 +1,17 @@
import { Schema } from 'mongoose';
import { dbserver } from '../../db/db.connect';
export class AuthRaw {
_id?: string;
jwt?: string;
apikey?: string;
}
const authSchema = new Schema<AuthRaw>({
_id: { type: String, _id: true },
jwt: { type: String, minlength: 1 },
apikey: { type: String, minlength: 1 },
});
export const AuthModel = dbserver?.model(AuthRaw.name, authSchema, 'authentication');
export type IAuthModel = typeof AuthModel;

View File

@@ -0,0 +1,18 @@
import { Schema } from 'mongoose';
import { dbserver } from '../../db/db.connect';
export class ChatRaw {
_id?: string;
id?: string;
owner: string;
lastMsgTimestamp?: number;
}
const chatSchema = new Schema<ChatRaw>({
_id: { type: String, _id: true },
id: { type: String, required: true, minlength: 1 },
owner: { type: String, required: true, minlength: 1 },
});
export const ChatModel = dbserver?.model(ChatRaw.name, chatSchema, 'chats');
export type IChatModel = typeof ChatModel;

View File

@@ -0,0 +1,21 @@
import { Schema } from 'mongoose';
import { dbserver } from '../../db/db.connect';
export class ContactRaw {
_id?: string;
pushName?: string;
id?: string;
profilePictureUrl?: string;
owner: string;
}
const contactSchema = new Schema<ContactRaw>({
_id: { type: String, _id: true },
pushName: { type: String, minlength: 1 },
id: { type: String, required: true, minlength: 1 },
profilePictureUrl: { type: String, minlength: 1 },
owner: { type: String, required: true, minlength: 1 },
});
export const ContactModel = dbserver?.model(ContactRaw.name, contactSchema, 'contacts');
export type IContactModel = typeof ContactModel;

View File

@@ -0,0 +1,5 @@
export * from './chat.model';
export * from './contact.model';
export * from './message.model';
export * from './auth.model';
export * from './webhook.model';

View File

@@ -0,0 +1,71 @@
import { Schema } from 'mongoose';
import { dbserver } from '../../db/db.connect';
import { wa } from '../types/wa.types';
class Key {
id?: string;
remoteJid?: string;
fromMe?: boolean;
participant?: string;
}
export class MessageRaw {
_id?: string;
key?: Key;
pushName?: string;
participant?: string;
message?: object;
messageType?: string;
messageTimestamp?: number | Long.Long;
owner: string;
source?: 'android' | 'web' | 'ios';
}
const messageSchema = new Schema<MessageRaw>({
_id: { type: String, _id: true },
key: {
id: { type: String, required: true, minlength: 1 },
remoteJid: { type: String, required: true, minlength: 1 },
fromMe: { type: Boolean, required: true },
participant: { type: String, minlength: 1 },
},
pushName: { type: String },
participant: { type: String },
messageType: { type: String },
message: { type: Object },
source: { type: String, minlength: 3, enum: ['android', 'web', 'ios'] },
messageTimestamp: { type: Number, required: true },
owner: { type: String, required: true, minlength: 1 },
});
export const MessageModel = dbserver?.model(MessageRaw.name, messageSchema, 'messages');
export type IMessageModel = typeof MessageModel;
export class MessageUpdateRaw {
_id?: string;
remoteJid?: string;
id?: string;
fromMe?: boolean;
participant?: string;
datetime?: number;
status?: wa.StatusMessage;
owner: string;
}
const messageUpdateSchema = new Schema<MessageUpdateRaw>({
_id: { type: String, _id: true },
remoteJid: { type: String, required: true, min: 1 },
id: { type: String, required: true, min: 1 },
fromMe: { type: Boolean, required: true },
participant: { type: String, min: 1 },
datetime: { type: Number, required: true, min: 1 },
status: { type: String, required: true },
owner: { type: String, required: true, min: 1 },
});
export const MessageUpModel = dbserver?.model(
MessageUpdateRaw.name,
messageUpdateSchema,
'messageUpdate',
);
export type IMessageUpModel = typeof MessageUpModel;

View File

@@ -0,0 +1,17 @@
import { Schema } from 'mongoose';
import { dbserver } from '../../db/db.connect';
export class WebhookRaw {
_id?: string;
url?: string;
enabled?: boolean;
}
const webhookSchema = new Schema<WebhookRaw>({
_id: { type: String, _id: true },
url: { type: String, required: true },
enabled: { type: Boolean, required: true },
});
export const WebhookModel = dbserver?.model(WebhookRaw.name, webhookSchema, 'webhook');
export type IWebhookModel = typeof WebhookModel;

View File

@@ -0,0 +1,57 @@
import { join } from 'path';
import { Auth, ConfigService } from '../../config/env.config';
import { IInsert, Repository } from '../abstract/abstract.repository';
import { IAuthModel, AuthRaw } from '../models';
import { readFileSync } from 'fs';
import { AUTH_DIR } from '../../config/path.config';
export class AuthRepository extends Repository {
constructor(
private readonly authModel: IAuthModel,
readonly configService: ConfigService,
) {
super(configService);
this.auth = configService.get<Auth>('AUTHENTICATION');
}
private readonly auth: Auth;
public async create(data: AuthRaw, instance: string): Promise<IInsert> {
try {
if (this.dbSettings.ENABLED) {
const insert = await this.authModel.replaceOne(
{ _id: instance },
{ ...data },
{ upsert: true },
);
return { insertCount: insert.modifiedCount };
}
this.writeStore<AuthRaw>({
path: join(AUTH_DIR, this.auth.TYPE),
fileName: instance,
data,
});
return { insertCount: 1 };
} catch (error) {
return { error } as any;
}
}
public async find(instance: string): Promise<AuthRaw> {
try {
if (this.dbSettings.ENABLED) {
return await this.authModel.findOne({ _id: instance });
}
return JSON.parse(
readFileSync(join(AUTH_DIR, this.auth.TYPE, instance + '.json'), {
encoding: 'utf-8',
}),
) as AuthRaw;
} catch (error) {
return {};
}
}
}

View File

@@ -0,0 +1,89 @@
import { join } from 'path';
import { ConfigService } from '../../config/env.config';
import { IInsert, Repository } from '../abstract/abstract.repository';
import { opendirSync, readFileSync, rmSync } from 'fs';
import { ChatRaw, IChatModel } from '../models';
export class ChatQuery {
where: ChatRaw;
}
export class ChatRepository extends Repository {
constructor(
private readonly chatModel: IChatModel,
private readonly configService: ConfigService,
) {
super(configService);
}
public async insert(data: ChatRaw[], saveDb = false): Promise<IInsert> {
if (data.length === 0) {
return;
}
try {
if (this.dbSettings.ENABLED && saveDb) {
const insert = await this.chatModel.insertMany([...data]);
return { insertCount: insert.length };
}
data.forEach((chat) => {
this.writeStore<ChatRaw>({
path: join(this.storePath, 'chats', chat.owner),
fileName: chat.id,
data: chat,
});
});
return { insertCount: data.length };
} catch (error) {
return error;
} finally {
data = undefined;
}
}
public async find(query: ChatQuery): Promise<ChatRaw[]> {
try {
if (this.dbSettings.ENABLED) {
return await this.chatModel.find({ owner: query.where.owner });
}
const chats: ChatRaw[] = [];
const openDir = opendirSync(join(this.storePath, 'chats', query.where.owner));
for await (const dirent of openDir) {
if (dirent.isFile()) {
chats.push(
JSON.parse(
readFileSync(
join(this.storePath, 'chats', query.where.owner, dirent.name),
{ encoding: 'utf-8' },
),
),
);
}
}
return chats;
} catch (error) {
return [];
}
}
public async delete(query: ChatQuery) {
try {
if (this.dbSettings.ENABLED) {
return await this.chatModel.deleteOne({ ...query.where });
}
rmSync(join(this.storePath, 'chats', query.where.owner, query.where.id + '.josn'), {
force: true,
recursive: true,
});
return { deleted: { chatId: query.where.id } };
} catch (error) {
return { error: error?.toString() };
}
}
}

View File

@@ -0,0 +1,88 @@
import { opendirSync, readFileSync } from 'fs';
import { join } from 'path';
import { ConfigService } from '../../config/env.config';
import { ContactRaw, IContactModel } from '../models';
import { IInsert, Repository } from '../abstract/abstract.repository';
export class ContactQuery {
where: ContactRaw;
}
export class ContactRepository extends Repository {
constructor(
private readonly contactModel: IContactModel,
private readonly configService: ConfigService,
) {
super(configService);
}
public async insert(data: ContactRaw[], saveDb = false): Promise<IInsert> {
if (data.length === 0) {
return;
}
try {
if (this.dbSettings.ENABLED && saveDb) {
const insert = await this.contactModel.insertMany([...data]);
return { insertCount: insert.length };
}
data.forEach((contact) => {
this.writeStore({
path: join(this.storePath, 'contacts', contact.owner),
fileName: contact.id,
data: contact,
});
});
return { insertCount: data.length };
} catch (error) {
return error;
} finally {
data = undefined;
}
}
public async find(query: ContactQuery): Promise<ContactRaw[]> {
try {
if (this.dbSettings.ENABLED) {
return await this.contactModel.find({ ...query.where });
}
const contacts: ContactRaw[] = [];
if (query?.where?.id) {
contacts.push(
JSON.parse(
readFileSync(
join(
this.storePath,
'contacts',
query.where.owner,
query.where.id + '.json',
),
{ encoding: 'utf-8' },
),
),
);
} else {
const openDir = opendirSync(join(this.storePath, 'contacts', query.where.owner), {
encoding: 'utf-8',
});
for await (const dirent of openDir) {
if (dirent.isFile()) {
contacts.push(
JSON.parse(
readFileSync(
join(this.storePath, 'contacts', query.where.owner, dirent.name),
{ encoding: 'utf-8' },
),
),
);
}
}
}
return contacts;
} catch (error) {
return [];
}
}
}

View File

@@ -0,0 +1,129 @@
import { ConfigService } from '../../config/env.config';
import { join } from 'path';
import { IMessageModel, MessageRaw } from '../models';
import { IInsert, Repository } from '../abstract/abstract.repository';
import { opendirSync, readFileSync } from 'fs';
export class MessageQuery {
where: MessageRaw;
limit?: number;
}
export class MessageRepository extends Repository {
constructor(
private readonly messageModel: IMessageModel,
private readonly configService: ConfigService,
) {
super(configService);
}
public async insert(data: MessageRaw[], saveDb = false): Promise<IInsert> {
if (!Array.isArray(data) || data.length === 0) {
return;
}
try {
if (this.dbSettings.ENABLED && saveDb) {
const cleanedData = data.map((obj) => {
const cleanedObj = { ...obj };
if ('extendedTextMessage' in obj.message) {
const extendedTextMessage = obj.message.extendedTextMessage as {
contextInfo?: {
mentionedJid?: any;
};
};
if (typeof extendedTextMessage === 'object' && extendedTextMessage !== null) {
if ('contextInfo' in extendedTextMessage) {
delete extendedTextMessage.contextInfo?.mentionedJid;
extendedTextMessage.contextInfo = {};
}
}
}
return cleanedObj;
});
const insert = await this.messageModel.insertMany([...cleanedData]);
return { insertCount: insert.length };
}
if (saveDb) {
data.forEach((msg) =>
this.writeStore<MessageRaw>({
path: join(this.storePath, 'messages', msg.owner),
fileName: msg.key.id,
data: msg,
}),
);
return { insertCount: data.length };
}
return { insertCount: 0 };
} catch (error) {
console.log('ERROR: ', error);
return error;
} finally {
data = undefined;
}
}
public async find(query: MessageQuery) {
try {
if (this.dbSettings.ENABLED) {
if (query?.where?.key) {
for (const [k, v] of Object.entries(query.where.key)) {
query.where['key.' + k] = v;
}
delete query?.where?.key;
}
return await this.messageModel
.find({ ...query.where })
.sort({ messageTimestamp: -1 })
.limit(query?.limit ?? 0);
}
const messages: MessageRaw[] = [];
if (query?.where?.key?.id) {
messages.push(
JSON.parse(
readFileSync(
join(
this.storePath,
'messages',
query.where.owner,
query.where.key.id + '.json',
),
{ encoding: 'utf-8' },
),
),
);
} else {
const openDir = opendirSync(join(this.storePath, 'messages', query.where.owner), {
encoding: 'utf-8',
});
for await (const dirent of openDir) {
if (dirent.isFile()) {
messages.push(
JSON.parse(
readFileSync(
join(this.storePath, 'messages', query.where.owner, dirent.name),
{ encoding: 'utf-8' },
),
),
);
}
}
}
return messages
.sort((x, y) => {
return (y.messageTimestamp as number) - (x.messageTimestamp as number);
})
.splice(0, query?.limit ?? messages.length);
} catch (error) {
return [];
}
}
}

View File

@@ -0,0 +1,96 @@
import { ConfigService } from '../../config/env.config';
import { IMessageUpModel, MessageUpdateRaw } from '../models';
import { IInsert, Repository } from '../abstract/abstract.repository';
import { join } from 'path';
import { opendirSync, readFileSync } from 'fs';
export class MessageUpQuery {
where: MessageUpdateRaw;
limit?: number;
}
export class MessageUpRepository extends Repository {
constructor(
private readonly messageUpModel: IMessageUpModel,
private readonly configService: ConfigService,
) {
super(configService);
}
public async insert(data: MessageUpdateRaw[], saveDb?: boolean): Promise<IInsert> {
if (data.length === 0) {
return;
}
try {
if (this.dbSettings.ENABLED && saveDb) {
const insert = await this.messageUpModel.insertMany([...data]);
return { insertCount: insert.length };
}
data.forEach((update) => {
this.writeStore<MessageUpdateRaw>({
path: join(this.storePath, 'message-up', update.owner),
fileName: update.id,
data: update,
});
});
} catch (error) {
return error;
}
}
public async find(query: MessageUpQuery) {
try {
if (this.dbSettings.ENABLED) {
return await this.messageUpModel
.find({ ...query.where })
.sort({ datetime: -1 })
.limit(query?.limit ?? 0);
}
const messageUpdate: MessageUpdateRaw[] = [];
if (query?.where?.id) {
messageUpdate.push(
JSON.parse(
readFileSync(
join(
this.storePath,
'message-up',
query.where.owner,
query.where.id + '.json',
),
{ encoding: 'utf-8' },
),
),
);
} else {
const openDir = opendirSync(
join(this.storePath, 'message-up', query.where.owner),
{ encoding: 'utf-8' },
);
for await (const dirent of openDir) {
if (dirent.isFile()) {
messageUpdate.push(
JSON.parse(
readFileSync(
join(this.storePath, 'message-up', query.where.owner, dirent.name),
{ encoding: 'utf-8' },
),
),
);
}
}
}
return messageUpdate
.sort((x, y) => {
return y.datetime - x.datetime;
})
.splice(0, query?.limit ?? messageUpdate.length);
} catch (error) {
return [];
}
}
}

View File

@@ -0,0 +1,27 @@
import { MessageRepository } from './message.repository';
import { ChatRepository } from './chat.repository';
import { ContactRepository } from './contact.repository';
import { MessageUpRepository } from './messageUp.repository';
import { MongoClient } from 'mongodb';
import { WebhookRepository } from './webhook.repository';
import { AuthRepository } from './auth.repository';
export class RepositoryBroker {
constructor(
public readonly message: MessageRepository,
public readonly chat: ChatRepository,
public readonly contact: ContactRepository,
public readonly messageUpdate: MessageUpRepository,
public readonly webhook: WebhookRepository,
public readonly auth: AuthRepository,
dbServer?: MongoClient,
) {
this.dbClient = dbServer;
}
private dbClient?: MongoClient;
public get dbServer() {
return this.dbClient;
}
}

View File

@@ -0,0 +1,53 @@
import { IInsert, Repository } from '../abstract/abstract.repository';
import { ConfigService } from '../../config/env.config';
import { join } from 'path';
import { readFileSync } from 'fs';
import { IWebhookModel, WebhookRaw } from '../models';
export class WebhookRepository extends Repository {
constructor(
private readonly webhookModel: IWebhookModel,
private readonly configService: ConfigService,
) {
super(configService);
}
public async create(data: WebhookRaw, instance: string): Promise<IInsert> {
try {
if (this.dbSettings.ENABLED) {
const insert = await this.webhookModel.replaceOne(
{ _id: instance },
{ ...data },
{ upsert: true },
);
return { insertCount: insert.modifiedCount };
}
this.writeStore<WebhookRaw>({
path: join(this.storePath, 'webhook'),
fileName: instance,
data,
});
return { insertCount: 1 };
} catch (error) {
return error;
}
}
public async find(instance: string): Promise<WebhookRaw> {
try {
if (this.dbSettings.ENABLED) {
return await this.webhookModel.findOne({ _id: instance });
}
return JSON.parse(
readFileSync(join(this.storePath, 'webhook', instance + '.json'), {
encoding: 'utf-8',
}),
) as WebhookRaw;
} catch (error) {
return {};
}
}
}

View File

@@ -0,0 +1,198 @@
import { RequestHandler, Router } from 'express';
import {
archiveChatSchema,
contactValidateSchema,
deleteMessageSchema,
messageUpSchema,
messageValidateSchema,
profileNameSchema,
profilePictureSchema,
profileStatusSchema,
readMessageSchema,
whatsappNumberSchema,
} from '../../validate/validate.schema';
import {
ArchiveChatDto,
DeleteMessage,
NumberDto,
ProfileNameDto,
ProfilePictureDto,
ProfileStatusDto,
ReadMessageDto,
WhatsAppNumberDto,
} from '../dto/chat.dto';
import { ContactQuery } from '../repository/contact.repository';
import { MessageQuery } from '../repository/message.repository';
import { chatController } from '../whatsapp.module';
import { RouterBroker } from '../abstract/abstract.router';
import { HttpStatus } from './index.router';
import { MessageUpQuery } from '../repository/messageUp.repository';
import { proto } from '@evolution/base';
import { InstanceDto } from '../dto/instance.dto';
export class ChatRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('whatsappNumbers'), ...guards, async (req, res) => {
const response = await this.dataValidate<WhatsAppNumberDto>({
request: req,
schema: whatsappNumberSchema,
ClassRef: WhatsAppNumberDto,
execute: (instance, data) => chatController.whatsappNumber(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
})
.put(this.routerPath('markMessageAsRead'), ...guards, async (req, res) => {
const response = await this.dataValidate<ReadMessageDto>({
request: req,
schema: readMessageSchema,
ClassRef: ReadMessageDto,
execute: (instance, data) => chatController.readMessage(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
})
.put(this.routerPath('archiveChat'), ...guards, async (req, res) => {
const response = await this.dataValidate<ArchiveChatDto>({
request: req,
schema: archiveChatSchema,
ClassRef: ArchiveChatDto,
execute: (instance, data) => chatController.archiveChat(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
})
.delete(
this.routerPath('deleteMessageForEveryone'),
...guards,
async (req, res) => {
const response = await this.dataValidate<DeleteMessage>({
request: req,
schema: deleteMessageSchema,
ClassRef: DeleteMessage,
execute: (instance, data) => chatController.deleteMessage(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
},
)
.post(this.routerPath('fetchProfilePictureUrl'), ...guards, async (req, res) => {
const response = await this.dataValidate<NumberDto>({
request: req,
schema: profilePictureSchema,
ClassRef: NumberDto,
execute: (instance, data) => chatController.fetchProfilePicture(instance, data),
});
return res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('findContacts'), ...guards, async (req, res) => {
const response = await this.dataValidate<ContactQuery>({
request: req,
schema: contactValidateSchema,
ClassRef: ContactQuery,
execute: (instance, data) => chatController.fetchContacts(instance, data),
});
return res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('getBase64FromMediaMessage'), ...guards, async (req, res) => {
const response = await this.dataValidate<proto.IWebMessageInfo>({
request: req,
schema: null,
ClassRef: Object,
execute: (instance, data) =>
chatController.getBase64FromMediaMessage(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('findMessages'), ...guards, async (req, res) => {
const response = await this.dataValidate<MessageQuery>({
request: req,
schema: messageValidateSchema,
ClassRef: MessageQuery,
execute: (instance, data) => chatController.fetchMessages(instance, data),
});
return res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('findStatusMessage'), ...guards, async (req, res) => {
const response = await this.dataValidate<MessageUpQuery>({
request: req,
schema: messageUpSchema,
ClassRef: MessageUpQuery,
execute: (instance, data) => chatController.fetchStatusMessage(instance, data),
});
return res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('findChats'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: null,
ClassRef: InstanceDto,
execute: (instance) => chatController.fetchChats(instance),
});
return res.status(HttpStatus.OK).json(response);
})
// Profile routes
.post(this.routerPath('getBusinessProfile'), ...guards, async (req, res) => {
const response = await this.dataValidate<ProfilePictureDto>({
request: req,
schema: profilePictureSchema,
ClassRef: ProfilePictureDto,
execute: (instance, data) => chatController.getBusinessProfile(instance, data),
});
return res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('updateProfileName'), ...guards, async (req, res) => {
const response = await this.dataValidate<ProfileNameDto>({
request: req,
schema: profileNameSchema,
ClassRef: ProfileNameDto,
execute: (instance, data) => chatController.updateProfileName(instance, data),
});
return res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('updateProfileStatus'), ...guards, async (req, res) => {
const response = await this.dataValidate<ProfileStatusDto>({
request: req,
schema: profileStatusSchema,
ClassRef: ProfileStatusDto,
execute: (instance, data) => chatController.updateProfileStatus(instance, data),
});
return res.status(HttpStatus.OK).json(response);
})
.put(this.routerPath('updateProfilePicture'), ...guards, async (req, res) => {
const response = await this.dataValidate<ProfilePictureDto>({
request: req,
schema: profilePictureSchema,
ClassRef: ProfilePictureDto,
execute: (instance, data) =>
chatController.updateProfilePicture(instance, data),
});
return res.status(HttpStatus.OK).json(response);
})
.delete(this.routerPath('removeProfilePicture'), ...guards, async (req, res) => {
const response = await this.dataValidate<ProfilePictureDto>({
request: req,
schema: profilePictureSchema,
ClassRef: ProfilePictureDto,
execute: (instance, data) =>
chatController.removeProfilePicture(instance, data),
});
return res.status(HttpStatus.OK).json(response);
});
}
public readonly router = Router();
}

View File

@@ -0,0 +1,141 @@
import { RequestHandler, Router } from 'express';
import {
createGroupSchema,
groupJidSchema,
updateParticipantsSchema,
updateSettingsSchema,
toggleEphemeralSchema,
updateGroupPicture,
groupInviteSchema,
} from '../../validate/validate.schema';
import { RouterBroker } from '../abstract/abstract.router';
import {
CreateGroupDto,
GroupInvite,
GroupJid,
GroupPictureDto,
GroupUpdateParticipantDto,
GroupUpdateSettingDto,
GroupToggleEphemeralDto,
} from '../dto/group.dto';
import { groupController } from '../whatsapp.module';
import { HttpStatus } from './index.router';
export class GroupRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('create'), ...guards, async (req, res) => {
const response = await this.dataValidate<CreateGroupDto>({
request: req,
schema: createGroupSchema,
ClassRef: CreateGroupDto,
execute: (instance, data) => groupController.createGroup(instance, data),
});
res.status(HttpStatus.CREATED).json(response);
})
.put(this.routerPath('updateGroupPicture'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupPictureDto>({
request: req,
schema: updateGroupPicture,
ClassRef: GroupPictureDto,
execute: (instance, data) => groupController.updateGroupPicture(instance, data),
});
res.status(HttpStatus.CREATED).json(response);
})
.get(this.routerPath('findGroupInfos'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupJid>({
request: req,
schema: groupJidSchema,
ClassRef: GroupJid,
execute: (instance, data) => groupController.findGroupInfo(instance, data),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('participants'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupJid>({
request: req,
schema: groupJidSchema,
ClassRef: GroupJid,
execute: (instance, data) => groupController.findParticipants(instance, data),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('inviteCode'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupJid>({
request: req,
schema: groupJidSchema,
ClassRef: GroupJid,
execute: (instance, data) => groupController.inviteCode(instance, data),
});
res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('inviteInfo'), ...guards, async (req, res) => {
const response = await this.inviteCodeValidate<GroupInvite>({
request: req,
schema: groupInviteSchema,
ClassRef: GroupInvite,
execute: (instance, data) => groupController.inviteInfo(instance, data),
});
res.status(HttpStatus.OK).json(response);
})
.put(this.routerPath('revokeInviteCode'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupJid>({
request: req,
schema: groupJidSchema,
ClassRef: GroupJid,
execute: (instance, data) => groupController.revokeInviteCode(instance, data),
});
res.status(HttpStatus.CREATED).json(response);
})
.put(this.routerPath('updateParticipant'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupUpdateParticipantDto>({
request: req,
schema: updateParticipantsSchema,
ClassRef: GroupUpdateParticipantDto,
execute: (instance, data) => groupController.updateGParticipate(instance, data),
});
res.status(HttpStatus.CREATED).json(response);
})
.put(this.routerPath('updateSetting'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupUpdateSettingDto>({
request: req,
schema: updateSettingsSchema,
ClassRef: GroupUpdateSettingDto,
execute: (instance, data) => groupController.updateGSetting(instance, data),
});
res.status(HttpStatus.CREATED).json(response);
})
.put(this.routerPath('toggleEphemeral'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupToggleEphemeralDto>({
request: req,
schema: toggleEphemeralSchema,
ClassRef: GroupToggleEphemeralDto,
execute: (instance, data) => groupController.toggleEphemeral(instance, data),
});
res.status(HttpStatus.CREATED).json(response);
})
.delete(this.routerPath('leaveGroup'), ...guards, async (req, res) => {
const response = await this.groupValidate<GroupJid>({
request: req,
schema: {},
ClassRef: GroupJid,
execute: (instance, data) => groupController.leaveGroup(instance, data),
});
res.status(HttpStatus.OK).json(response);
});
}
public readonly router = Router();
}

View File

@@ -0,0 +1,37 @@
import { Router } from 'express';
import { Auth, configService } from '../../config/env.config';
import { instanceExistsGuard, instanceLoggedGuard } from '../guards/instance.guard';
import { authGuard } from '../guards/auth.guard';
import { ChatRouter } from './chat.router';
import { GroupRouter } from './group.router';
import { InstanceRouter } from './instance.router';
import { MessageRouter } from './sendMessage.router';
import { ViewsRouter } from './view.router';
import { WebhookRouter } from './webhook.router';
enum HttpStatus {
OK = 200,
CREATED = 201,
NOT_FOUND = 404,
FORBIDDEN = 403,
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
INTERNAL_SERVER_ERROR = 500,
}
const router = Router();
const authType = configService.get<Auth>('AUTHENTICATION').TYPE;
const guards = [instanceExistsGuard, instanceLoggedGuard, authGuard[authType]];
router
.use(
'/instance',
new InstanceRouter(configService, ...guards).router,
new ViewsRouter(instanceExistsGuard).router,
)
.use('/message', new MessageRouter(...guards).router)
.use('/chat', new ChatRouter(...guards).router)
.use('/group', new GroupRouter(...guards).router)
.use('/webhook', new WebhookRouter(...guards).router);
export { router, HttpStatus };

View File

@@ -0,0 +1,113 @@
import { RequestHandler, Router } from 'express';
import { instanceNameSchema, oldTokenSchema } from '../../validate/validate.schema';
import { InstanceDto } from '../dto/instance.dto';
import { instanceController } from '../whatsapp.module';
import { RouterBroker } from '../abstract/abstract.router';
import { HttpStatus } from './index.router';
import { OldToken } from '../services/auth.service';
import { Auth, ConfigService, Database } from '../../config/env.config';
import { dbserver } from '../../db/db.connect';
import { BadRequestException, InternalServerErrorException } from '../../exceptions';
export class InstanceRouter extends RouterBroker {
constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) {
super();
const auth = configService.get<Auth>('AUTHENTICATION');
this.router
.post('/create', ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceNameSchema,
ClassRef: InstanceDto,
execute: (instance) => instanceController.createInstance(instance),
});
return res.status(HttpStatus.CREATED).json(response);
})
.get(this.routerPath('connect'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceNameSchema,
ClassRef: InstanceDto,
execute: (instance) => instanceController.connectToWhatsapp(instance),
});
return res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('connectionState'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceNameSchema,
ClassRef: InstanceDto,
execute: (instance) => instanceController.connectionState(instance),
});
return res.status(HttpStatus.OK).json(response);
})
.get(this.routerPath('fetchInstances', false), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: null,
ClassRef: InstanceDto,
execute: (instance) => instanceController.fetchInstances(instance),
});
return res.status(HttpStatus.OK).json(response);
})
.delete(this.routerPath('logout'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceNameSchema,
ClassRef: InstanceDto,
execute: (instance) => instanceController.logout(instance),
});
return res.status(HttpStatus.OK).json(response);
})
.delete(this.routerPath('delete'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceNameSchema,
ClassRef: InstanceDto,
execute: (instance) => instanceController.deleteInstance(instance),
});
return res.status(HttpStatus.OK).json(response);
});
if (auth.TYPE === 'jwt') {
this.router.put('/refreshToken', async (req, res) => {
const response = await this.dataValidate<OldToken>({
request: req,
schema: oldTokenSchema,
ClassRef: OldToken,
execute: (_, data) => instanceController.refreshToken(_, data),
});
return res.status(HttpStatus.CREATED).json(response);
});
}
this.router.delete('/deleteDatabase', async (req, res) => {
const db = this.configService.get<Database>('DATABASE');
if (db.ENABLED) {
try {
await dbserver.dropDatabase();
return res
.status(HttpStatus.CREATED)
.json({ error: false, message: 'Database deleted' });
} catch (error) {
return res
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.json({ error: true, message: error.message });
}
}
return res
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.json({ error: true, message: 'Database is not enabled' });
});
}
public readonly router = Router();
}

View File

@@ -0,0 +1,139 @@
import { RequestHandler, Router } from 'express';
import {
audioMessageSchema,
buttonMessageSchema,
contactMessageSchema,
linkPreviewSchema,
listMessageSchema,
locationMessageSchema,
mediaMessageSchema,
pollMessageSchema,
reactionMessageSchema,
textMessageSchema,
} from '../../validate/validate.schema';
import {
SendAudioDto,
SendButtonDto,
SendContactDto,
SendLinkPreviewDto,
SendListDto,
SendLocationDto,
SendMediaDto,
SendPollDto,
SendReactionDto,
SendTextDto,
} from '../dto/sendMessage.dto';
import { sendMessageController } from '../whatsapp.module';
import { RouterBroker } from '../abstract/abstract.router';
import { HttpStatus } from './index.router';
export class MessageRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('sendText'), ...guards, async (req, res) => {
const response = await this.dataValidate<SendTextDto>({
request: req,
schema: textMessageSchema,
ClassRef: SendTextDto,
execute: (instance, data) => sendMessageController.sendText(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendMedia'), ...guards, async (req, res) => {
const response = await this.dataValidate<SendMediaDto>({
request: req,
schema: mediaMessageSchema,
ClassRef: SendMediaDto,
execute: (instance, data) => sendMessageController.sendMedia(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendWhatsAppAudio'), ...guards, async (req, res) => {
const response = await this.dataValidate<SendAudioDto>({
request: req,
schema: audioMessageSchema,
ClassRef: SendMediaDto,
execute: (instance, data) =>
sendMessageController.sendWhatsAppAudio(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendButtons'), ...guards, async (req, res) => {
const response = await this.dataValidate<SendButtonDto>({
request: req,
schema: buttonMessageSchema,
ClassRef: SendButtonDto,
execute: (instance, data) => sendMessageController.sendButtons(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendLocation'), ...guards, async (req, res) => {
const response = await this.dataValidate<SendLocationDto>({
request: req,
schema: locationMessageSchema,
ClassRef: SendLocationDto,
execute: (instance, data) => sendMessageController.sendLocation(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendList'), ...guards, async (req, res) => {
const response = await this.dataValidate<SendListDto>({
request: req,
schema: listMessageSchema,
ClassRef: SendListDto,
execute: (instance, data) => sendMessageController.sendList(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendContact'), ...guards, async (req, res) => {
const response = await this.dataValidate<SendContactDto>({
request: req,
schema: contactMessageSchema,
ClassRef: SendContactDto,
execute: (instance, data) => sendMessageController.sendContact(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendReaction'), ...guards, async (req, res) => {
const response = await this.dataValidate<SendReactionDto>({
request: req,
schema: reactionMessageSchema,
ClassRef: SendReactionDto,
execute: (instance, data) => sendMessageController.sendReaction(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendPoll'), ...guards, async (req, res) => {
const response = await this.dataValidate<SendPollDto>({
request: req,
schema: pollMessageSchema,
ClassRef: SendPollDto,
execute: (instance, data) => sendMessageController.sendPoll(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendLinkPreview'), ...guards, async (req, res) => {
const response = await this.dataValidate<SendLinkPreviewDto>({
request: req,
schema: linkPreviewSchema,
ClassRef: SendLinkPreviewDto,
execute: (instance, data) =>
sendMessageController.sendLinkPreview(instance, data),
});
return res.status(HttpStatus.CREATED).json(response);
});
}
public readonly router = Router();
}

View File

@@ -0,0 +1,15 @@
import { RequestHandler, Router } from 'express';
import { RouterBroker } from '../abstract/abstract.router';
import { viewsController } from '../whatsapp.module';
export class ViewsRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router.get(this.routerPath('qrcode'), ...guards, (req, res) => {
return viewsController.qrcode(req, res);
});
}
public readonly router = Router();
}

View File

@@ -0,0 +1,36 @@
import { RequestHandler, Router } from 'express';
import { instanceNameSchema, webhookSchema } from '../../validate/validate.schema';
import { RouterBroker } from '../abstract/abstract.router';
import { InstanceDto } from '../dto/instance.dto';
import { WebhookDto } from '../dto/webhook.dto';
import { webhookController } from '../whatsapp.module';
import { HttpStatus } from './index.router';
export class WebhookRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('set'), ...guards, async (req, res) => {
const response = await this.dataValidate<WebhookDto>({
request: req,
schema: webhookSchema,
ClassRef: WebhookDto,
execute: (instance, data) => webhookController.createWebhook(instance, data),
});
res.status(HttpStatus.CREATED).json(response);
})
.get(this.routerPath('find'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceNameSchema,
ClassRef: InstanceDto,
execute: (instance) => webhookController.findWebhook(instance),
});
res.status(HttpStatus.OK).json(response);
});
}
public readonly router = Router();
}

View File

@@ -0,0 +1,136 @@
import { Auth, ConfigService, Webhook } from '../../config/env.config';
import { InstanceDto } from '../dto/instance.dto';
import { name as apiName } from '../../../package.json';
import { verify, sign } from 'jsonwebtoken';
import { Logger } from '../../config/logger.config';
import { v4 } from 'uuid';
import { isJWT } from 'class-validator';
import { BadRequestException } from '../../exceptions';
import axios from 'axios';
import { WAMonitoringService } from './monitor.service';
import { RepositoryBroker } from '../repository/repository.manager';
export type JwtPayload = {
instanceName: string;
apiName: string;
jwt?: string;
apikey?: string;
tokenId: string;
};
export class OldToken {
oldToken: string;
}
export class AuthService {
constructor(
private readonly configService: ConfigService,
private readonly waMonitor: WAMonitoringService,
private readonly repository: RepositoryBroker,
) {}
private readonly logger = new Logger(AuthService.name);
private async jwt(instance: InstanceDto) {
const jwtOpts = this.configService.get<Auth>('AUTHENTICATION').JWT;
const token = sign(
{
instanceName: instance.instanceName,
apiName,
tokenId: v4(),
},
jwtOpts.SECRET,
{ expiresIn: jwtOpts.EXPIRIN_IN, encoding: 'utf8', subject: 'g-t' },
);
const auth = await this.repository.auth.create({ jwt: token }, instance.instanceName);
if (auth['error']) {
this.logger.error({
localError: AuthService.name + '.jwt',
error: auth['error'],
});
throw new BadRequestException('Authentication error', auth['error']?.toString());
}
return { jwt: token };
}
private async apikey(instance: InstanceDto) {
const apikey = v4().toUpperCase();
const auth = await this.repository.auth.create({ apikey }, instance.instanceName);
if (auth['error']) {
this.logger.error({
localError: AuthService.name + '.jwt',
error: auth['error'],
});
throw new BadRequestException('Authentication error', auth['error']?.toString());
}
return { apikey };
}
public async generateHash(instance: InstanceDto) {
const options = this.configService.get<Auth>('AUTHENTICATION');
return (await this[options.TYPE](instance)) as { jwt: string } | { apikey: string };
}
public async refreshToken({ oldToken }: OldToken) {
if (!isJWT(oldToken)) {
throw new BadRequestException('Invalid "oldToken"');
}
try {
const jwtOpts = this.configService.get<Auth>('AUTHENTICATION').JWT;
const decode = verify(oldToken, jwtOpts.SECRET, {
ignoreExpiration: true,
}) as Pick<JwtPayload, 'apiName' | 'instanceName' | 'tokenId'>;
const tokenStore = await this.repository.auth.find(decode.instanceName);
const decodeTokenStore = verify(tokenStore.jwt, jwtOpts.SECRET, {
ignoreExpiration: true,
}) as Pick<JwtPayload, 'apiName' | 'instanceName' | 'tokenId'>;
if (decode.tokenId !== decodeTokenStore.tokenId) {
throw new BadRequestException('Invalid "oldToken"');
}
const token = {
jwt: (await this.jwt({ instanceName: decode.instanceName })).jwt,
instanceName: decode.instanceName,
};
try {
const webhook = await this.repository.webhook.find(decode.instanceName);
if (
webhook?.enabled &&
this.configService.get<Webhook>('WEBHOOK').EVENTS.NEW_JWT_TOKEN
) {
const httpService = axios.create({ baseURL: webhook.url });
await httpService.post(
'',
{
event: 'new.jwt',
instance: decode.instanceName,
data: token,
},
{ params: { owner: this.waMonitor.waInstances[decode.instanceName].wuid } },
);
}
} catch (error) {
this.logger.error(error);
}
return token;
} catch (error) {
this.logger.error({
localError: AuthService.name + '.refreshToken',
error,
});
throw new BadRequestException('Invalid "oldToken"');
}
}
}

View File

@@ -0,0 +1,216 @@
import { opendirSync, readdirSync, rmSync } from 'fs';
import { WAStartupService } from './whatsapp.service';
import { INSTANCE_DIR } from '../../config/path.config';
import EventEmitter2 from 'eventemitter2';
import { join } from 'path';
import { Logger } from '../../config/logger.config';
import { ConfigService, Database, DelInstance, Redis } from '../../config/env.config';
import { RepositoryBroker } from '../repository/repository.manager';
import { NotFoundException } from '../../exceptions';
import { Db } from 'mongodb';
import { RedisCache } from '../../db/redis.client';
import { initInstance } from '../whatsapp.module';
import { ValidationError } from 'class-validator';
export class WAMonitoringService {
constructor(
private readonly eventEmitter: EventEmitter2,
private readonly configService: ConfigService,
private readonly repository: RepositoryBroker,
) {
this.removeInstance();
this.noConnection();
this.delInstanceFiles();
Object.assign(this.db, configService.get<Database>('DATABASE'));
Object.assign(this.redis, configService.get<Redis>('REDIS'));
this.dbInstance = this.db.ENABLED
? this.repository.dbServer?.db(this.db.CONNECTION.DB_PREFIX_NAME + '-instances')
: undefined;
this.redisCache = this.redis.ENABLED ? new RedisCache(this.redis) : undefined;
}
private readonly db: Partial<Database> = {};
private readonly redis: Partial<Redis> = {};
private dbInstance: Db;
private redisCache: RedisCache;
private readonly logger = new Logger(WAMonitoringService.name);
public readonly waInstances: Record<string, WAStartupService> = {};
public delInstanceTime(instance: string) {
const time = this.configService.get<DelInstance>('DEL_INSTANCE');
if (typeof time === 'number' && time > 0) {
setTimeout(() => {
if (this.waInstances[instance]?.connectionStatus?.state !== 'open') {
delete this.waInstances[instance];
}
}, 1000 * 60 * time);
}
}
public async instanceInfo(instanceName?: string) {
if (instanceName && !this.waInstances[instanceName]) {
throw new NotFoundException(`Instance "${instanceName}" not found`);
}
const instances: any[] = [];
for await (const [key, value] of Object.entries(this.waInstances)) {
if (value && value.connectionStatus.state === 'open') {
instances.push({
instance: {
instanceName: key,
owner: value.wuid,
profileName: (await value.getProfileName()) || 'not loaded',
profilePictureUrl: value.profilePictureUrl,
status: (await value.getProfileStatus()) || '',
},
});
}
}
return instances.find((i) => i.instance.instanceName === instanceName) ?? instances;
}
private delInstanceFiles() {
setInterval(async () => {
if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) {
const collections = await this.dbInstance.collections();
collections.forEach(async (collection) => {
const name = collection.namespace.replace(/^[\w-]+./, '');
await this.dbInstance.collection(name).deleteMany({
$or: [
{ _id: { $regex: /^app.state.*/ } },
{ _id: { $regex: /^session-.*/ } },
],
});
});
} else {
const dir = opendirSync(INSTANCE_DIR, { encoding: 'utf-8' });
for await (const dirent of dir) {
if (dirent.isDirectory()) {
const files = readdirSync(join(INSTANCE_DIR, dirent.name), {
encoding: 'utf-8',
});
files.forEach(async (file) => {
if (file.match(/^app.state.*/) || file.match(/^session-.*/)) {
rmSync(join(INSTANCE_DIR, dirent.name, file), {
recursive: true,
force: true,
});
}
});
}
}
}
}, 3600 * 1000 * 2);
}
private async cleaningUp(instanceName: string) {
if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) {
await this.repository.dbServer.connect();
const collections: any[] = await this.dbInstance.collections();
if (collections.length > 0) {
await this.dbInstance.dropCollection(instanceName);
}
return;
}
if (this.redis.ENABLED) {
this.redisCache.reference = instanceName;
await this.redisCache.delAll();
return;
}
rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true });
}
public async loadInstance() {
const set = async (name: string) => {
const instance = new WAStartupService(
this.configService,
this.eventEmitter,
this.repository,
);
instance.instanceName = name;
await instance.connectToWhatsapp();
this.waInstances[name] = instance;
};
try {
if (this.redis.ENABLED) {
const keys = await this.redisCache.instanceKeys();
if (keys?.length > 0) {
keys.forEach(async (k) => await set(k.split(':')[1]));
} else {
initInstance();
}
return;
}
if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) {
await this.repository.dbServer.connect();
const collections: any[] = await this.dbInstance.collections();
if (collections.length > 0) {
collections.forEach(
async (coll) => await set(coll.namespace.replace(/^[\w-]+\./, '')),
);
} else {
initInstance();
}
return;
}
const dir = opendirSync(INSTANCE_DIR, { encoding: 'utf-8' });
for await (const dirent of dir) {
if (dirent.isDirectory()) {
const files = readdirSync(join(INSTANCE_DIR, dirent.name), {
encoding: 'utf-8',
});
if (files.length === 0) {
rmSync(join(INSTANCE_DIR, dirent.name), { recursive: true, force: true });
break;
}
await set(dirent.name);
}
}
} catch (error) {
this.logger.error(error);
}
}
private removeInstance() {
this.eventEmitter.on('remove.instance', async (instanceName: string) => {
try {
this.waInstances[instanceName] = undefined;
} catch {}
try {
this.cleaningUp(instanceName);
} finally {
this.logger.warn(`Instance "${instanceName}" - REMOVED`);
}
});
}
private noConnection() {
this.eventEmitter.on('no.connection', async (instanceName) => {
try {
this.waInstances[instanceName] = undefined;
this.cleaningUp(instanceName);
} catch (error) {
this.logger.error({
localError: 'noConnection',
warn: 'Error deleting instance from memory.',
error,
});
} finally {
this.logger.warn(`Instance "${instanceName}" - NOT CONNECTION`);
}
});
}
}

View File

@@ -0,0 +1,21 @@
import { InstanceDto } from '../dto/instance.dto';
import { WebhookDto } from '../dto/webhook.dto';
import { WAMonitoringService } from './monitor.service';
export class WebhookService {
constructor(private readonly waMonitor: WAMonitoringService) {}
public create(instance: InstanceDto, data: WebhookDto) {
this.waMonitor.waInstances[instance.instanceName].setWebhook(data);
return { webhook: { ...instance, webhook: data } };
}
public async find(instance: InstanceDto): Promise<WebhookDto> {
try {
return await this.waMonitor.waInstances[instance.instanceName].findWebhook();
} catch (error) {
return { enabled: null, url: '' };
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,67 @@
/* eslint-disable @typescript-eslint/no-namespace */
import { AuthenticationState, WAConnectionState } from '@evolution/base';
export enum Events {
APPLICATION_STARTUP = 'application.startup',
QRCODE_UPDATED = 'qrcode.updated',
CONNECTION_UPDATE = 'connection.update',
STATUS_INSTANCE = 'status.instance',
MESSAGES_SET = 'messages.set',
MESSAGES_UPSERT = 'messages.upsert',
MESSAGES_UPDATE = 'messages.update',
SEND_MESSAGE = 'send.message',
CONTACTS_SET = 'contacts.set',
CONTACTS_UPSERT = 'contacts.upsert',
CONTACTS_UPDATE = 'contacts.update',
PRESENCE_UPDATE = 'presence.update',
CHATS_SET = 'chats.set',
CHATS_UPDATE = 'chats.update',
CHATS_UPSERT = 'chats.upsert',
CHATS_DELETE = 'chats.delete',
GROUPS_UPSERT = 'groups.upsert',
GROUPS_UPDATE = 'groups.update',
GROUP_PARTICIPANTS_UPDATE = 'group-participants.update',
}
export declare namespace wa {
export type QrCode = { count?: number; base64?: string; code?: string };
export type Instance = {
qrcode?: QrCode;
authState?: { state: AuthenticationState; saveCreds: () => void };
name?: string;
wuid?: string;
profileName?: string;
profilePictureUrl?: string;
};
export type LocalWebHook = { enabled?: boolean; url?: string };
export type StateConnection = {
instance?: string;
state?: WAConnectionState | 'refused';
statusReason?: number;
};
export type StatusMessage =
| 'ERROR'
| 'PENDING'
| 'SERVER_ACK'
| 'DELIVERY_ACK'
| 'READ'
| 'PLAYED';
}
export const TypeMediaMessage = [
'imageMessage',
'documentMessage',
'audioMessage',
'videoMessage',
'stickerMessage',
];
export const MessageSubtype = [
'ephemeralMessage',
'documentWithCaptionMessage',
'viewOnceMessage',
'viewOnceMessageV2',
];

View File

@@ -0,0 +1,143 @@
import { Auth, configService } from '../config/env.config';
import { Logger } from '../config/logger.config';
import { eventEmitter } from '../config/event.config';
import { MessageRepository } from './repository/message.repository';
import { WAMonitoringService } from './services/monitor.service';
import { ChatRepository } from './repository/chat.repository';
import { ContactRepository } from './repository/contact.repository';
import { MessageUpRepository } from './repository/messageUp.repository';
import { ChatController } from './controllers/chat.controller';
import { InstanceController } from './controllers/instance.controller';
import { SendMessageController } from './controllers/sendMessage.controller';
import { AuthService } from './services/auth.service';
import { GroupController } from './controllers/group.controller';
import { ViewsController } from './controllers/views.controller';
import { WebhookService } from './services/webhook.service';
import { WebhookController } from './controllers/webhook.controller';
import { RepositoryBroker } from './repository/repository.manager';
import {
AuthModel,
ChatModel,
ContactModel,
MessageModel,
MessageUpModel,
} from './models';
import { dbserver } from '../db/db.connect';
import { WebhookRepository } from './repository/webhook.repository';
import { WebhookModel } from './models/webhook.model';
import { AuthRepository } from './repository/auth.repository';
import { WAStartupService } from './services/whatsapp.service';
import { delay } from '@evolution/base';
import { Events } from './types/wa.types';
const logger = new Logger('WA MODULE');
const messageRepository = new MessageRepository(MessageModel, configService);
const chatRepository = new ChatRepository(ChatModel, configService);
const contactRepository = new ContactRepository(ContactModel, configService);
const messageUpdateRepository = new MessageUpRepository(MessageUpModel, configService);
const webhookRepository = new WebhookRepository(WebhookModel, configService);
const authRepository = new AuthRepository(AuthModel, configService);
export const repository = new RepositoryBroker(
messageRepository,
chatRepository,
contactRepository,
messageUpdateRepository,
webhookRepository,
authRepository,
dbserver?.getClient(),
);
export const waMonitor = new WAMonitoringService(eventEmitter, configService, repository);
const authService = new AuthService(configService, waMonitor, repository);
const webhookService = new WebhookService(waMonitor);
export const webhookController = new WebhookController(webhookService);
export const instanceController = new InstanceController(
waMonitor,
configService,
repository,
eventEmitter,
authService,
webhookService,
);
export const viewsController = new ViewsController(waMonitor, configService);
export const sendMessageController = new SendMessageController(waMonitor);
export const chatController = new ChatController(waMonitor);
export const groupController = new GroupController(waMonitor);
export async function initInstance() {
const instance = new WAStartupService(configService, eventEmitter, repository);
const mode = configService.get<Auth>('AUTHENTICATION').INSTANCE.MODE;
instance.sendDataWebhook(
Events.APPLICATION_STARTUP,
{
message: 'Application startup',
mode,
},
false,
);
if (mode === 'container') {
const instanceName = configService.get<Auth>('AUTHENTICATION').INSTANCE.NAME;
const instanceWebhook =
configService.get<Auth>('AUTHENTICATION').INSTANCE.WEBHOOK_URL;
instance.instanceName = instanceName;
waMonitor.waInstances[instance.instanceName] = instance;
waMonitor.delInstanceTime(instance.instanceName);
const hash = await authService.generateHash({
instanceName: instance.instanceName,
});
if (instanceWebhook) {
try {
webhookService.create(instance, { enabled: true, url: instanceWebhook });
} catch (error) {
this.logger.log(error);
}
}
try {
const state = instance.connectionStatus?.state;
switch (state) {
case 'close':
await instance.connectToWhatsapp();
await delay(2000);
return instance.qrCode;
case 'connecting':
return instance.qrCode;
default:
return await this.connectionState({ instanceName });
}
} catch (error) {
this.logger.log(error);
}
const result = {
instance: {
instanceName: instance.instanceName,
status: 'created',
},
hash,
webhook: instanceWebhook,
};
logger.info(result);
return result;
}
return null;
}
logger.info('Module - ON');