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