mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-25 22:57:44 -06:00
init project evolution api
This commit is contained in:
136
src/whatsapp/services/auth.service.ts
Normal file
136
src/whatsapp/services/auth.service.ts
Normal 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"');
|
||||
}
|
||||
}
|
||||
}
|
||||
216
src/whatsapp/services/monitor.service.ts
Normal file
216
src/whatsapp/services/monitor.service.ts
Normal 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`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
21
src/whatsapp/services/webhook.service.ts
Normal file
21
src/whatsapp/services/webhook.service.ts
Normal 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: '' };
|
||||
}
|
||||
}
|
||||
}
|
||||
1663
src/whatsapp/services/whatsapp.service.ts
Normal file
1663
src/whatsapp/services/whatsapp.service.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user