mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-13 15:14:49 -06:00
New method of saving sessions to a file using worker
This commit is contained in:
parent
f7c9541f5e
commit
696261d749
@ -1,3 +1,8 @@
|
||||
# 1.8.1 (develop)
|
||||
|
||||
### Feature
|
||||
* New method of saving sessions to a file using worker, made in partnership with [codechat](https://github.com/code-chat-br/whatsapp-api)
|
||||
|
||||
# 1.8.0 (2024-05-27 16:10)
|
||||
|
||||
### Feature
|
||||
|
@ -33,10 +33,7 @@ CLEAN_STORE_CHATS=true
|
||||
|
||||
# Permanent data storage
|
||||
DATABASE_ENABLED=false
|
||||
DATABASE_CONNECTION_URI=mongodb://root:root@mongodb:27017/?authSource=admin &
|
||||
readPreference=primary &
|
||||
ssl=false &
|
||||
directConnection=true
|
||||
DATABASE_CONNECTION_URI=mongodb://root:root@mongodb:27017/?authSource=admin&readPreference=primary&ssl=false&directConnection=true
|
||||
DATABASE_CONNECTION_DB_PREFIX_NAME=evdocker
|
||||
|
||||
# Choose the data you want to save in the application's database or store
|
||||
@ -137,7 +134,7 @@ CONFIG_SESSION_PHONE_NAME=Chrome
|
||||
|
||||
# Set qrcode display limit
|
||||
QRCODE_LIMIT=30
|
||||
QRCODE_COLOR=#198754
|
||||
QRCODE_COLOR='#198754'
|
||||
|
||||
# old | latest
|
||||
TYPEBOT_API_VERSION=latest
|
||||
|
@ -12,6 +12,7 @@ import { RabbitmqService } from '../integrations/rabbitmq/services/rabbitmq.serv
|
||||
import { SqsService } from '../integrations/sqs/services/sqs.service';
|
||||
import { TypebotService } from '../integrations/typebot/services/typebot.service';
|
||||
import { WebsocketService } from '../integrations/websocket/services/websocket.service';
|
||||
import { ProviderFiles } from '../provider/sessions';
|
||||
import { RepositoryBroker } from '../repository/repository.manager';
|
||||
import { AuthService, OldToken } from '../services/auth.service';
|
||||
import { CacheService } from '../services/cache.service';
|
||||
@ -43,6 +44,7 @@ export class InstanceController {
|
||||
private readonly cache: CacheService,
|
||||
private readonly chatwootCache: CacheService,
|
||||
private readonly messagesLostCache: CacheService,
|
||||
private readonly providerFiles: ProviderFiles,
|
||||
) {}
|
||||
|
||||
private readonly logger = new Logger(InstanceController.name);
|
||||
@ -111,6 +113,7 @@ export class InstanceController {
|
||||
this.cache,
|
||||
this.chatwootCache,
|
||||
this.messagesLostCache,
|
||||
this.providerFiles,
|
||||
);
|
||||
} else {
|
||||
instance = new BaileysStartupService(
|
||||
@ -120,6 +123,7 @@ export class InstanceController {
|
||||
this.cache,
|
||||
this.chatwootCache,
|
||||
this.messagesLostCache,
|
||||
this.providerFiles,
|
||||
);
|
||||
}
|
||||
|
||||
|
140
src/api/provider/sessions.ts
Normal file
140
src/api/provider/sessions.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import axios from 'axios';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
import { ConfigService, ProviderSession } from '../../config/env.config';
|
||||
import { Logger } from '../../config/logger.config';
|
||||
|
||||
type ResponseSuccess = { status: number; data?: any };
|
||||
type ResponseProvider = Promise<[ResponseSuccess?, Error?]>;
|
||||
|
||||
export class ProviderFiles {
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.baseUrl = `http://${this.config.HOST}:${this.config.PORT}/session`;
|
||||
}
|
||||
|
||||
private readonly logger = new Logger(ProviderFiles.name);
|
||||
|
||||
private baseUrl: string;
|
||||
|
||||
private readonly config = Object.freeze(this.configService.get<ProviderSession>('PROVIDER'));
|
||||
|
||||
private readonly prefix = Object.freeze(this.configService.get<ProviderSession>('PROVIDER').PREFIX);
|
||||
|
||||
get isEnabled() {
|
||||
return !!this.config?.ENABLED;
|
||||
}
|
||||
|
||||
public async onModuleInit() {
|
||||
if (this.config.ENABLED) {
|
||||
const client = axios.create({
|
||||
baseURL: this.baseUrl,
|
||||
});
|
||||
try {
|
||||
const response = await client.options('/ping');
|
||||
if (!response?.data?.pong) {
|
||||
throw new Error('Offline file provider.');
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(['Failed to connect to the file server', error?.message, error?.stack]);
|
||||
const pid = process.pid;
|
||||
execSync(`kill -9 ${pid}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async onModuleDestroy() {
|
||||
//
|
||||
}
|
||||
|
||||
public async create(instance: string): ResponseProvider {
|
||||
try {
|
||||
const response = await axios.post(`${this.baseUrl}/${this.prefix}`, {
|
||||
instance,
|
||||
});
|
||||
return [{ status: response.status, data: response?.data }];
|
||||
} catch (error) {
|
||||
return [
|
||||
{
|
||||
status: error?.response?.status,
|
||||
data: error?.response?.data,
|
||||
},
|
||||
error,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public async write(instance: string, key: string, data: any): ResponseProvider {
|
||||
try {
|
||||
const response = await axios.post(`${this.baseUrl}/${this.prefix}/${instance}/${key}`, data);
|
||||
return [{ status: response.status, data: response?.data }];
|
||||
} catch (error) {
|
||||
return [
|
||||
{
|
||||
status: error?.response?.status,
|
||||
data: error?.response?.data,
|
||||
},
|
||||
error,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public async read(instance: string, key: string): ResponseProvider {
|
||||
try {
|
||||
const response = await axios.get(`${this.baseUrl}/${this.prefix}/${instance}/${key}`);
|
||||
return [{ status: response.status, data: response?.data }];
|
||||
} catch (error) {
|
||||
return [
|
||||
{
|
||||
status: error?.response?.status,
|
||||
data: error?.response?.data,
|
||||
},
|
||||
error,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public async delete(instance: string, key: string): ResponseProvider {
|
||||
try {
|
||||
const response = await axios.delete(`${this.baseUrl}/${this.prefix}/${instance}/${key}`);
|
||||
return [{ status: response.status, data: response?.data }];
|
||||
} catch (error) {
|
||||
return [
|
||||
{
|
||||
status: error?.response?.status,
|
||||
data: error?.response?.data,
|
||||
},
|
||||
error,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public async allInstances(): ResponseProvider {
|
||||
try {
|
||||
const response = await axios.get(`${this.baseUrl}/list-instances/${this.prefix}`);
|
||||
return [{ status: response.status, data: response?.data as string[] }];
|
||||
} catch (error) {
|
||||
return [
|
||||
{
|
||||
status: error?.response?.status,
|
||||
data: error?.response?.data,
|
||||
},
|
||||
error,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public async removeSession(instance: string): ResponseProvider {
|
||||
try {
|
||||
const response = await axios.delete(`${this.baseUrl}/${this.prefix}/${instance}`);
|
||||
return [{ status: response.status, data: response?.data }];
|
||||
} catch (error) {
|
||||
return [
|
||||
{
|
||||
status: error?.response?.status,
|
||||
data: error?.response?.data,
|
||||
},
|
||||
error,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
@ -47,6 +47,7 @@ import {
|
||||
WebsocketModel,
|
||||
} from './models';
|
||||
import { LabelModel } from './models/label.model';
|
||||
import { ProviderFiles } from './provider/sessions';
|
||||
import { AuthRepository } from './repository/auth.repository';
|
||||
import { ChatRepository } from './repository/chat.repository';
|
||||
import { ContactRepository } from './repository/contact.repository';
|
||||
@ -109,6 +110,7 @@ export const repository = new RepositoryBroker(
|
||||
export const cache = new CacheService(new CacheEngine(configService, 'instance').getEngine());
|
||||
const chatwootCache = new CacheService(new CacheEngine(configService, ChatwootService.name).getEngine());
|
||||
const messagesLostCache = new CacheService(new CacheEngine(configService, 'baileys').getEngine());
|
||||
const providerFiles = new ProviderFiles(configService);
|
||||
|
||||
export const waMonitor = new WAMonitoringService(
|
||||
eventEmitter,
|
||||
@ -117,6 +119,7 @@ export const waMonitor = new WAMonitoringService(
|
||||
cache,
|
||||
chatwootCache,
|
||||
messagesLostCache,
|
||||
providerFiles,
|
||||
);
|
||||
|
||||
const authService = new AuthService(configService, waMonitor, repository);
|
||||
@ -168,6 +171,7 @@ export const instanceController = new InstanceController(
|
||||
cache,
|
||||
chatwootCache,
|
||||
messagesLostCache,
|
||||
providerFiles,
|
||||
);
|
||||
export const sendMessageController = new SendMessageController(waMonitor);
|
||||
export const chatController = new ChatController(waMonitor);
|
||||
|
@ -55,12 +55,21 @@ import qrcode, { QRCodeToDataURLOptions } from 'qrcode';
|
||||
import qrcodeTerminal from 'qrcode-terminal';
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { CacheConf, ConfigService, ConfigSessionPhone, Database, Log, QrCode } from '../../../config/env.config';
|
||||
import {
|
||||
CacheConf,
|
||||
ConfigService,
|
||||
ConfigSessionPhone,
|
||||
Database,
|
||||
Log,
|
||||
ProviderSession,
|
||||
QrCode,
|
||||
} from '../../../config/env.config';
|
||||
import { INSTANCE_DIR } from '../../../config/path.config';
|
||||
import { BadRequestException, InternalServerErrorException, NotFoundException } from '../../../exceptions';
|
||||
import { dbserver } from '../../../libs/db.connect';
|
||||
import { makeProxyAgent } from '../../../utils/makeProxyAgent';
|
||||
import { useMultiFileAuthStateDb } from '../../../utils/use-multi-file-auth-state-db';
|
||||
import { AuthStateProvider } from '../../../utils/use-multi-file-auth-state-provider-files';
|
||||
import { useMultiFileAuthStateRedisDb } from '../../../utils/use-multi-file-auth-state-redis-db';
|
||||
import {
|
||||
ArchiveChatDto,
|
||||
@ -114,6 +123,7 @@ import { SettingsRaw } from '../../models';
|
||||
import { ChatRaw } from '../../models/chat.model';
|
||||
import { ContactRaw } from '../../models/contact.model';
|
||||
import { MessageRaw, MessageUpdateRaw } from '../../models/message.model';
|
||||
import { ProviderFiles } from '../../provider/sessions';
|
||||
import { RepositoryBroker } from '../../repository/repository.manager';
|
||||
import { waMonitor } from '../../server.module';
|
||||
import { Events, MessageSubtype, TypeMediaMessage, wa } from '../../types/wa.types';
|
||||
@ -128,6 +138,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
public readonly cache: CacheService,
|
||||
public readonly chatwootCache: CacheService,
|
||||
public readonly messagesLostCache: CacheService,
|
||||
private readonly providerFiles: ProviderFiles,
|
||||
) {
|
||||
super(configService, eventEmitter, repository, chatwootCache);
|
||||
this.logger.verbose('BaileysStartupService initialized');
|
||||
@ -135,8 +146,10 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
this.instance.qrcode = { count: 0 };
|
||||
this.mobile = false;
|
||||
this.recoveringMessages();
|
||||
this.authStateProvider = new AuthStateProvider(this.configService, this.providerFiles);
|
||||
}
|
||||
|
||||
private authStateProvider: AuthStateProvider;
|
||||
private readonly msgRetryCounterCache: CacheStore = new NodeCache();
|
||||
private readonly userDevicesCache: CacheStore = new NodeCache();
|
||||
private endSession = false;
|
||||
@ -461,6 +474,12 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
const db = this.configService.get<Database>('DATABASE');
|
||||
const cache = this.configService.get<CacheConf>('CACHE');
|
||||
|
||||
const provider = this.configService.get<ProviderSession>('PROVIDER');
|
||||
|
||||
if (provider?.ENABLED) {
|
||||
return await this.authStateProvider.authStateProvider(this.instance.name);
|
||||
}
|
||||
|
||||
if (cache?.REDIS.ENABLED && cache?.REDIS.SAVE_INSTANCES) {
|
||||
this.logger.info('Redis enabled');
|
||||
return await useMultiFileAuthStateRedisDb(this.instance.name, this.cache);
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
SendTextDto,
|
||||
} from '../../dto/sendMessage.dto';
|
||||
import { ContactRaw, MessageRaw, MessageUpdateRaw, SettingsRaw } from '../../models';
|
||||
import { ProviderFiles } from '../../provider/sessions';
|
||||
import { RepositoryBroker } from '../../repository/repository.manager';
|
||||
import { Events, wa } from '../../types/wa.types';
|
||||
import { CacheService } from './../cache.service';
|
||||
@ -36,6 +37,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
public readonly cache: CacheService,
|
||||
public readonly chatwootCache: CacheService,
|
||||
public readonly messagesLostCache: CacheService,
|
||||
private readonly providerFiles: ProviderFiles,
|
||||
) {
|
||||
super(configService, eventEmitter, repository, chatwootCache);
|
||||
this.logger.verbose('BusinessStartupService initialized');
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
WebhookModel,
|
||||
WebsocketModel,
|
||||
} from '../models';
|
||||
import { ProviderFiles } from '../provider/sessions';
|
||||
import { RepositoryBroker } from '../repository/repository.manager';
|
||||
import { Integration } from '../types/wa.types';
|
||||
import { CacheService } from './cache.service';
|
||||
@ -36,6 +37,7 @@ export class WAMonitoringService {
|
||||
private readonly cache: CacheService,
|
||||
private readonly chatwootCache: CacheService,
|
||||
private readonly messagesLostCache: CacheService,
|
||||
private readonly providerFiles: ProviderFiles,
|
||||
) {
|
||||
this.logger.verbose('instance created');
|
||||
|
||||
@ -349,6 +351,7 @@ export class WAMonitoringService {
|
||||
this.cache,
|
||||
this.chatwootCache,
|
||||
this.messagesLostCache,
|
||||
this.providerFiles,
|
||||
);
|
||||
|
||||
instance.instanceName = name;
|
||||
@ -360,6 +363,7 @@ export class WAMonitoringService {
|
||||
this.cache,
|
||||
this.chatwootCache,
|
||||
this.messagesLostCache,
|
||||
this.providerFiles,
|
||||
);
|
||||
|
||||
instance.instanceName = name;
|
||||
|
@ -28,6 +28,13 @@ export type Log = {
|
||||
BAILEYS: LogBaileys;
|
||||
};
|
||||
|
||||
export type ProviderSession = {
|
||||
ENABLED: boolean;
|
||||
HOST: string;
|
||||
PORT: string;
|
||||
PREFIX: string;
|
||||
};
|
||||
|
||||
export type SaveData = {
|
||||
INSTANCE: boolean;
|
||||
NEW_MESSAGE: boolean;
|
||||
@ -209,6 +216,7 @@ export interface Env {
|
||||
SERVER: HttpServer;
|
||||
CORS: Cors;
|
||||
SSL_CONF: SslConf;
|
||||
PROVIDER: ProviderSession;
|
||||
STORE: StoreConf;
|
||||
CLEAN_STORE: CleanStoreConf;
|
||||
DATABASE: Database;
|
||||
@ -274,6 +282,12 @@ export class ConfigService {
|
||||
PRIVKEY: process.env?.SSL_CONF_PRIVKEY || '',
|
||||
FULLCHAIN: process.env?.SSL_CONF_FULLCHAIN || '',
|
||||
},
|
||||
PROVIDER: {
|
||||
ENABLED: process.env?.PROVIDER_ENABLED === 'true',
|
||||
HOST: process.env.PROVIDER_HOST,
|
||||
PORT: process.env?.PROVIDER_PORT || '5656',
|
||||
PREFIX: process.env?.PROVIDER_PREFIX || 'evolution',
|
||||
},
|
||||
STORE: {
|
||||
MESSAGES: process.env?.STORE_MESSAGES === 'true',
|
||||
MESSAGE_UP: process.env?.STORE_MESSAGE_UP === 'true',
|
||||
|
@ -49,6 +49,14 @@ LOG:
|
||||
DEL_INSTANCE: false # or false
|
||||
DEL_TEMP_INSTANCES: true # Delete instances with status closed on start
|
||||
|
||||
# Seesion Files Providers
|
||||
# Provider responsible for managing credentials files and WhatsApp sessions.
|
||||
PROVIDER:
|
||||
ENABLED: true
|
||||
HOST: 127.0.0.1
|
||||
PORT: 5656
|
||||
PREFIX: evolution
|
||||
|
||||
# Temporary data storage
|
||||
STORE:
|
||||
MESSAGES: true
|
||||
|
@ -9,6 +9,7 @@ import { join } from 'path';
|
||||
import { initAMQP, initGlobalQueues } from './api/integrations/rabbitmq/libs/amqp.server';
|
||||
import { initSQS } from './api/integrations/sqs/libs/sqs.server';
|
||||
import { initIO } from './api/integrations/websocket/libs/socket.server';
|
||||
import { ProviderFiles } from './api/provider/sessions';
|
||||
import { HttpStatus, router } from './api/routes/index.router';
|
||||
import { waMonitor } from './api/server.module';
|
||||
import { Auth, configService, Cors, HttpServer, Rabbitmq, Sqs, Webhook } from './config/env.config';
|
||||
@ -22,10 +23,14 @@ function initWA() {
|
||||
waMonitor.loadInstance();
|
||||
}
|
||||
|
||||
function bootstrap() {
|
||||
async function bootstrap() {
|
||||
const logger = new Logger('SERVER');
|
||||
const app = express();
|
||||
|
||||
const providerFiles = new ProviderFiles(configService);
|
||||
await providerFiles.onModuleInit();
|
||||
logger.info('Provider:Files - ON');
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin(requestOrigin, callback) {
|
||||
|
140
src/utils/use-multi-file-auth-state-provider-files.ts
Normal file
140
src/utils/use-multi-file-auth-state-provider-files.ts
Normal file
@ -0,0 +1,140 @@
|
||||
/**
|
||||
* ┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
* │ @author jrCleber │
|
||||
* │ @filename use-multi-file-auth-state-redis-db.ts │
|
||||
* │ Developed by: Cleber Wilson │
|
||||
* │ Creation date: Apr 09, 2023 │
|
||||
* │ Contact: contato@codechat.dev │
|
||||
* ├──────────────────────────────────────────────────────────────────────────────┤
|
||||
* │ @copyright © Cleber Wilson 2023. All rights reserved. │
|
||||
* │ Licensed under the Apache License, Version 2.0 │
|
||||
* │ │
|
||||
* │ @license "https://github.com/code-chat-br/whatsapp-api/blob/main/LICENSE" │
|
||||
* │ │
|
||||
* │ You may not use this file except in compliance with the License. │
|
||||
* │ You may obtain a copy of the License at │
|
||||
* │ │
|
||||
* │ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
* │ │
|
||||
* │ Unless required by applicable law or agreed to in writing, software │
|
||||
* │ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
* │ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
* │ │
|
||||
* │ See the License for the specific language governing permissions and │
|
||||
* │ limitations under the License. │
|
||||
* │ │
|
||||
* │ @type {AuthState} │
|
||||
* │ @function useMultiFileAuthStateRedisDb │
|
||||
* │ @returns {Promise<AuthState>} │
|
||||
* ├──────────────────────────────────────────────────────────────────────────────┤
|
||||
* │ @important │
|
||||
* │ For any future changes to the code in this file, it is recommended to │
|
||||
* │ contain, together with the modification, the information of the developer │
|
||||
* │ who changed it and the date of modification. │
|
||||
* └──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
|
||||
import {
|
||||
AuthenticationCreds,
|
||||
AuthenticationState,
|
||||
BufferJSON,
|
||||
initAuthCreds,
|
||||
proto,
|
||||
SignalDataTypeMap,
|
||||
} from '@whiskeysockets/baileys';
|
||||
import { isNotEmpty } from 'class-validator';
|
||||
|
||||
import { ProviderFiles } from '../api/provider/sessions';
|
||||
import { ConfigService } from '../config/env.config';
|
||||
import { Logger } from '../config/logger.config';
|
||||
|
||||
export type AuthState = { state: AuthenticationState; saveCreds: () => Promise<void> };
|
||||
|
||||
export class AuthStateProvider {
|
||||
constructor(private readonly configService: ConfigService, private readonly providerFiles: ProviderFiles) {}
|
||||
|
||||
private readonly logger = new Logger(AuthStateProvider.name);
|
||||
|
||||
public async authStateProvider(instance: string): Promise<AuthState> {
|
||||
const [, error] = await this.providerFiles.create(instance);
|
||||
if (error) {
|
||||
this.logger.error(['Failed to create folder on file server', error?.message, error?.stack]);
|
||||
return;
|
||||
}
|
||||
|
||||
const writeData = async (data: any, key: string): Promise<any> => {
|
||||
const json = JSON.stringify(data, BufferJSON.replacer);
|
||||
const [response, error] = await this.providerFiles.write(instance, key, {
|
||||
data: json,
|
||||
});
|
||||
if (error) {
|
||||
this.logger.error([error?.message, error?.stack]);
|
||||
return;
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
const readData = async (key: string): Promise<any> => {
|
||||
const [response, error] = await this.providerFiles.read(instance, key);
|
||||
if (error) {
|
||||
this.logger.error([error?.message, error?.stack]);
|
||||
return;
|
||||
}
|
||||
if (isNotEmpty(response?.data)) {
|
||||
return JSON.parse(JSON.stringify(response.data), BufferJSON.reviver);
|
||||
}
|
||||
};
|
||||
|
||||
const removeData = async (key: string) => {
|
||||
const [response, error] = await this.providerFiles.delete(instance, key);
|
||||
if (error) {
|
||||
this.logger.error([error?.message, error?.stack]);
|
||||
return;
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds();
|
||||
|
||||
return {
|
||||
state: {
|
||||
creds,
|
||||
keys: {
|
||||
get: async (type, ids: string[]) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: { [_: string]: SignalDataTypeMap[type] } = {};
|
||||
await Promise.all(
|
||||
ids.map(async (id) => {
|
||||
let value = await readData(`${type}-${id}`);
|
||||
if (type === 'app-state-sync-key' && value) {
|
||||
value = proto.Message.AppStateSyncKeyData.fromObject(value);
|
||||
}
|
||||
|
||||
data[id] = value;
|
||||
}),
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
set: async (data: any) => {
|
||||
const tasks: Promise<void>[] = [];
|
||||
for (const category in data) {
|
||||
for (const id in data[category]) {
|
||||
const value = data[category][id];
|
||||
const key = `${category}-${id}`;
|
||||
tasks.push(value ? await writeData(value, key) : await removeData(key));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(tasks);
|
||||
},
|
||||
},
|
||||
},
|
||||
saveCreds: async () => {
|
||||
return await writeData(creds, 'creds');
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user