mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-13 15:14:49 -06:00
refactor: update Flowise integration for improved configuration and validation
This commit refines the Flowise integration by enhancing configuration management and validation logic. Key changes include: - Reordered parameters in the FlowiseService constructor for consistency. - Updated FlowiseController to utilize the configService for integration enablement checks. - Simplified FlowiseDto and FlowiseSettingDto by removing unused properties. - Enhanced validation logic in flowise.schema.ts to include new fields. - Improved error handling in the createBot method to prevent duplicate entries. These updates contribute to a more robust and maintainable Flowise integration.
This commit is contained in:
parent
64fc7a05ac
commit
95bd85b6e3
@ -1,13 +1,16 @@
|
|||||||
|
import { InstanceDto } from '@api/dto/instance.dto';
|
||||||
import { PrismaRepository } from '@api/repository/repository.service';
|
import { PrismaRepository } from '@api/repository/repository.service';
|
||||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||||
|
import { configService, Flowise } from '@config/env.config';
|
||||||
import { Logger } from '@config/logger.config';
|
import { Logger } from '@config/logger.config';
|
||||||
import { Flowise, IntegrationSession } from '@prisma/client';
|
import { BadRequestException } from '@exceptions';
|
||||||
|
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
|
||||||
|
|
||||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||||
import { FlowiseDto } from '../dto/flowise.dto';
|
import { FlowiseDto } from '../dto/flowise.dto';
|
||||||
import { FlowiseService } from '../services/flowise.service';
|
import { FlowiseService } from '../services/flowise.service';
|
||||||
|
|
||||||
export class FlowiseController extends BaseChatbotController<Flowise, FlowiseDto> {
|
export class FlowiseController extends BaseChatbotController<FlowiseModel, FlowiseDto> {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly flowiseService: FlowiseService,
|
private readonly flowiseService: FlowiseService,
|
||||||
prismaRepository: PrismaRepository,
|
prismaRepository: PrismaRepository,
|
||||||
@ -23,14 +26,12 @@ export class FlowiseController extends BaseChatbotController<Flowise, FlowiseDto
|
|||||||
public readonly logger = new Logger('FlowiseController');
|
public readonly logger = new Logger('FlowiseController');
|
||||||
protected readonly integrationName = 'Flowise';
|
protected readonly integrationName = 'Flowise';
|
||||||
|
|
||||||
integrationEnabled = true; // Set to true by default or use config value if available
|
integrationEnabled = configService.get<Flowise>('FLOWISE').ENABLED;
|
||||||
botRepository: any;
|
botRepository: any;
|
||||||
settingsRepository: any;
|
settingsRepository: any;
|
||||||
sessionRepository: any;
|
sessionRepository: any;
|
||||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||||
|
|
||||||
// Implementation of abstract methods required by BaseChatbotController
|
|
||||||
|
|
||||||
protected getFallbackBotId(settings: any): string | undefined {
|
protected getFallbackBotId(settings: any): string | undefined {
|
||||||
return settings?.flowiseIdFallback;
|
return settings?.flowiseIdFallback;
|
||||||
}
|
}
|
||||||
@ -50,7 +51,6 @@ export class FlowiseController extends BaseChatbotController<Flowise, FlowiseDto
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementation for bot-specific updates
|
|
||||||
protected getAdditionalUpdateFields(data: FlowiseDto): Record<string, any> {
|
protected getAdditionalUpdateFields(data: FlowiseDto): Record<string, any> {
|
||||||
return {
|
return {
|
||||||
apiUrl: data.apiUrl,
|
apiUrl: data.apiUrl,
|
||||||
@ -58,13 +58,10 @@ export class FlowiseController extends BaseChatbotController<Flowise, FlowiseDto
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementation for bot-specific duplicate validation on update
|
|
||||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: FlowiseDto): Promise<void> {
|
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: FlowiseDto): Promise<void> {
|
||||||
const checkDuplicate = await this.botRepository.findFirst({
|
const checkDuplicate = await this.botRepository.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: {
|
id: { not: botId },
|
||||||
not: botId,
|
|
||||||
},
|
|
||||||
instanceId: instanceId,
|
instanceId: instanceId,
|
||||||
apiUrl: data.apiUrl,
|
apiUrl: data.apiUrl,
|
||||||
apiKey: data.apiKey,
|
apiKey: data.apiKey,
|
||||||
@ -76,16 +73,47 @@ export class FlowiseController extends BaseChatbotController<Flowise, FlowiseDto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process bot-specific logic
|
// Process Flowise-specific bot logic
|
||||||
protected async processBot(
|
protected async processBot(
|
||||||
instance: any,
|
instance: any,
|
||||||
remoteJid: string,
|
remoteJid: string,
|
||||||
bot: Flowise,
|
bot: FlowiseModel,
|
||||||
session: IntegrationSession,
|
session: IntegrationSession,
|
||||||
settings: any,
|
settings: any,
|
||||||
content: string,
|
content: string,
|
||||||
pushName?: string,
|
pushName?: string,
|
||||||
|
msg?: any,
|
||||||
) {
|
) {
|
||||||
await this.flowiseService.process(instance, remoteJid, bot, session, settings, content, pushName);
|
await this.flowiseService.processBot(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override createBot to add module availability check and Flowise-specific validation
|
||||||
|
public async createBot(instance: InstanceDto, data: FlowiseDto) {
|
||||||
|
if (!this.integrationEnabled)
|
||||||
|
throw new BadRequestException('Flowise is disabled');
|
||||||
|
|
||||||
|
const instanceId = await this.prismaRepository.instance
|
||||||
|
.findFirst({
|
||||||
|
where: {
|
||||||
|
name: instance.instanceName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((instance) => instance.id);
|
||||||
|
|
||||||
|
// Flowise-specific duplicate check
|
||||||
|
const checkDuplicate = await this.botRepository.findFirst({
|
||||||
|
where: {
|
||||||
|
instanceId: instanceId,
|
||||||
|
apiUrl: data.apiUrl,
|
||||||
|
apiKey: data.apiKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (checkDuplicate) {
|
||||||
|
throw new Error('Flowise already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let the base class handle the rest
|
||||||
|
return super.createBot(instance, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,39 +1,10 @@
|
|||||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
|
||||||
|
|
||||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||||
|
|
||||||
export class FlowiseDto extends BaseChatbotDto {
|
export class FlowiseDto extends BaseChatbotDto {
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
apiKey: string;
|
apiKey?: string;
|
||||||
description: string;
|
|
||||||
keywordFinish?: string | null;
|
|
||||||
triggerType: TriggerType;
|
|
||||||
enabled?: boolean;
|
|
||||||
expire?: number;
|
|
||||||
delayMessage?: number;
|
|
||||||
unknownMessage?: string;
|
|
||||||
listeningFromMe?: boolean;
|
|
||||||
stopBotFromMe?: boolean;
|
|
||||||
keepOpen?: boolean;
|
|
||||||
debounceTime?: number;
|
|
||||||
triggerOperator?: TriggerOperator;
|
|
||||||
triggerValue?: string;
|
|
||||||
ignoreJids?: any;
|
|
||||||
splitMessages?: boolean;
|
|
||||||
timePerChar?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FlowiseSettingDto extends BaseChatbotSettingDto {
|
export class FlowiseSettingDto extends BaseChatbotSettingDto {
|
||||||
expire?: number;
|
|
||||||
keywordFinish?: string | null;
|
|
||||||
delayMessage?: number;
|
|
||||||
unknownMessage?: string;
|
|
||||||
listeningFromMe?: boolean;
|
|
||||||
stopBotFromMe?: boolean;
|
|
||||||
keepOpen?: boolean;
|
|
||||||
debounceTime?: number;
|
|
||||||
flowiseIdFallback?: string;
|
flowiseIdFallback?: string;
|
||||||
ignoreJids?: any;
|
|
||||||
splitMessages?: boolean;
|
|
||||||
timePerChar?: number;
|
|
||||||
}
|
}
|
||||||
|
@ -2,133 +2,135 @@
|
|||||||
import { PrismaRepository } from '@api/repository/repository.service';
|
import { PrismaRepository } from '@api/repository/repository.service';
|
||||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||||
import { Integration } from '@api/types/wa.types';
|
import { Integration } from '@api/types/wa.types';
|
||||||
import { Auth, ConfigService, HttpServer } from '@config/env.config';
|
import { ConfigService, HttpServer } from '@config/env.config';
|
||||||
import { Flowise, FlowiseSetting, IntegrationSession } from '@prisma/client';
|
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
|
||||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||||
import { OpenaiService } from '../../openai/services/openai.service';
|
import { OpenaiService } from '../../openai/services/openai.service';
|
||||||
|
|
||||||
export class FlowiseService extends BaseChatbotService<Flowise, FlowiseSetting> {
|
export class FlowiseService extends BaseChatbotService<FlowiseModel> {
|
||||||
private openaiService: OpenaiService;
|
private openaiService: OpenaiService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
waMonitor: WAMonitoringService,
|
waMonitor: WAMonitoringService,
|
||||||
configService: ConfigService,
|
|
||||||
prismaRepository: PrismaRepository,
|
prismaRepository: PrismaRepository,
|
||||||
|
configService: ConfigService,
|
||||||
openaiService: OpenaiService,
|
openaiService: OpenaiService,
|
||||||
) {
|
) {
|
||||||
super(waMonitor, prismaRepository, 'FlowiseService', configService);
|
super(waMonitor, prismaRepository, 'FlowiseService', configService);
|
||||||
this.openaiService = openaiService;
|
this.openaiService = openaiService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Return the bot type for Flowise
|
||||||
* Get the bot type identifier
|
|
||||||
*/
|
|
||||||
protected getBotType(): string {
|
protected getBotType(): string {
|
||||||
return 'flowise';
|
return 'flowise';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Process Flowise-specific bot logic
|
||||||
* Send a message to the Flowise API
|
public async processBot(
|
||||||
*/
|
instance: any,
|
||||||
|
remoteJid: string,
|
||||||
|
bot: FlowiseModel,
|
||||||
|
session: IntegrationSession,
|
||||||
|
settings: any,
|
||||||
|
content: string,
|
||||||
|
pushName?: string,
|
||||||
|
msg?: any,
|
||||||
|
) {
|
||||||
|
await this.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement the abstract method to send message to Flowise API
|
||||||
protected async sendMessageToBot(
|
protected async sendMessageToBot(
|
||||||
instance: any,
|
instance: any,
|
||||||
session: IntegrationSession,
|
session: IntegrationSession,
|
||||||
settings: FlowiseSetting,
|
settings: any,
|
||||||
bot: Flowise,
|
bot: FlowiseModel,
|
||||||
remoteJid: string,
|
remoteJid: string,
|
||||||
pushName: string,
|
pushName: string,
|
||||||
content: string,
|
content: string,
|
||||||
msg?: any,
|
msg?: any,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
const payload: any = {
|
||||||
const payload: any = {
|
question: content,
|
||||||
question: content,
|
overrideConfig: {
|
||||||
overrideConfig: {
|
sessionId: remoteJid,
|
||||||
sessionId: remoteJid,
|
vars: {
|
||||||
vars: {
|
remoteJid: remoteJid,
|
||||||
remoteJid: remoteJid,
|
pushName: pushName,
|
||||||
pushName: pushName,
|
instanceName: instance.instanceName,
|
||||||
instanceName: instance.instanceName,
|
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
apiKey: instance.token,
|
||||||
apiKey: instance.token,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
if (this.isAudioMessage(content) && msg) {
|
// Handle audio messages
|
||||||
try {
|
if (this.isAudioMessage(content) && msg) {
|
||||||
this.logger.debug(`[EvolutionBot] Downloading audio for Whisper transcription`);
|
try {
|
||||||
const transcription = await this.openaiService.speechToText(msg);
|
this.logger.debug(`[Flowise] Downloading audio for Whisper transcription`);
|
||||||
if (transcription) {
|
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||||
payload.query = transcription;
|
if (transcription) {
|
||||||
} else {
|
payload.question = transcription;
|
||||||
payload.query = '[Audio message could not be transcribed]';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(`[EvolutionBot] Failed to transcribe audio: ${err}`);
|
|
||||||
payload.query = '[Audio message could not be transcribed]';
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`[Flowise] Failed to transcribe audio: ${err}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isImageMessage(content)) {
|
if (this.isImageMessage(content)) {
|
||||||
const contentSplit = content.split('|');
|
const contentSplit = content.split('|');
|
||||||
|
|
||||||
payload.uploads = [
|
payload.uploads = [
|
||||||
{
|
{
|
||||||
data: contentSplit[1].split('?')[0],
|
data: contentSplit[1].split('?')[0],
|
||||||
type: 'url',
|
type: 'url',
|
||||||
name: 'Flowise.png',
|
name: 'Flowise.png',
|
||||||
mime: 'image/png',
|
mime: 'image/png',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
payload.question = contentSplit[2] || content;
|
payload.question = contentSplit[2] || content;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||||
await instance.client.presenceSubscribe(remoteJid);
|
await instance.client.presenceSubscribe(remoteJid);
|
||||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||||
}
|
}
|
||||||
|
|
||||||
let headers: any = {
|
let headers: any = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bot.apiKey) {
|
||||||
|
headers = {
|
||||||
|
...headers,
|
||||||
|
Authorization: `Bearer ${bot.apiKey}`,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (bot.apiKey) {
|
const endpoint = bot.apiUrl;
|
||||||
headers = {
|
|
||||||
...headers,
|
|
||||||
Authorization: `Bearer ${bot.apiKey}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const endpoint = bot.apiUrl;
|
if (!endpoint) {
|
||||||
|
this.logger.error('No Flowise endpoint defined');
|
||||||
if (!endpoint) {
|
|
||||||
this.logger.error('No Flowise endpoint defined');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.post(endpoint, payload, {
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
|
||||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = response?.data?.text;
|
|
||||||
|
|
||||||
if (message) {
|
|
||||||
// Use the base class method to send the message to WhatsApp
|
|
||||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send telemetry
|
|
||||||
sendTelemetry('/message/sendText');
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Error in sendMessageToBot: ${error.message || JSON.stringify(error)}`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const response = await axios.post(endpoint, payload, {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||||
|
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = response?.data?.text;
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
// Use the base class method to send the message to WhatsApp
|
||||||
|
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The service is now complete with just the abstract method implementations
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,8 @@ export const flowiseSchema: JSONSchema7 = {
|
|||||||
keepOpen: { type: 'boolean' },
|
keepOpen: { type: 'boolean' },
|
||||||
debounceTime: { type: 'integer' },
|
debounceTime: { type: 'integer' },
|
||||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||||
|
splitMessages: { type: 'boolean' },
|
||||||
|
timePerChar: { type: 'integer' },
|
||||||
},
|
},
|
||||||
required: ['enabled', 'apiUrl', 'triggerType'],
|
required: ['enabled', 'apiUrl', 'triggerType'],
|
||||||
...isNotEmpty('enabled', 'apiUrl', 'triggerType'),
|
...isNotEmpty('enabled', 'apiUrl', 'triggerType'),
|
||||||
@ -69,7 +71,9 @@ export const flowiseSettingSchema: JSONSchema7 = {
|
|||||||
keepOpen: { type: 'boolean' },
|
keepOpen: { type: 'boolean' },
|
||||||
debounceTime: { type: 'integer' },
|
debounceTime: { type: 'integer' },
|
||||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||||
botIdFallback: { type: 'string' },
|
flowiseIdFallback: { type: 'string' },
|
||||||
|
splitMessages: { type: 'boolean' },
|
||||||
|
timePerChar: { type: 'integer' },
|
||||||
},
|
},
|
||||||
required: [
|
required: [
|
||||||
'expire',
|
'expire',
|
||||||
|
@ -129,7 +129,7 @@ export const difyController = new DifyController(difyService, prismaRepository,
|
|||||||
const evolutionBotService = new EvolutionBotService(waMonitor, configService, prismaRepository, openaiService);
|
const evolutionBotService = new EvolutionBotService(waMonitor, configService, prismaRepository, openaiService);
|
||||||
export const evolutionBotController = new EvolutionBotController(evolutionBotService, prismaRepository, waMonitor);
|
export const evolutionBotController = new EvolutionBotController(evolutionBotService, prismaRepository, waMonitor);
|
||||||
|
|
||||||
const flowiseService = new FlowiseService(waMonitor, configService, prismaRepository, openaiService);
|
const flowiseService = new FlowiseService(waMonitor, prismaRepository, configService, openaiService);
|
||||||
export const flowiseController = new FlowiseController(flowiseService, prismaRepository, waMonitor);
|
export const flowiseController = new FlowiseController(flowiseService, prismaRepository, waMonitor);
|
||||||
|
|
||||||
const n8nService = new N8nService(waMonitor, prismaRepository, configService, openaiService);
|
const n8nService = new N8nService(waMonitor, prismaRepository, configService, openaiService);
|
||||||
|
@ -270,6 +270,7 @@ export type Openai = { ENABLED: boolean; API_KEY_GLOBAL?: string };
|
|||||||
export type Dify = { ENABLED: boolean };
|
export type Dify = { ENABLED: boolean };
|
||||||
export type N8n = { ENABLED: boolean };
|
export type N8n = { ENABLED: boolean };
|
||||||
export type Evoai = { ENABLED: boolean };
|
export type Evoai = { ENABLED: boolean };
|
||||||
|
export type Flowise = { ENABLED: boolean };
|
||||||
|
|
||||||
export type S3 = {
|
export type S3 = {
|
||||||
ACCESS_KEY: string;
|
ACCESS_KEY: string;
|
||||||
@ -311,6 +312,7 @@ export interface Env {
|
|||||||
DIFY: Dify;
|
DIFY: Dify;
|
||||||
N8N: N8n;
|
N8N: N8n;
|
||||||
EVOAI: Evoai;
|
EVOAI: Evoai;
|
||||||
|
FLOWISE: Flowise;
|
||||||
CACHE: CacheConf;
|
CACHE: CacheConf;
|
||||||
S3?: S3;
|
S3?: S3;
|
||||||
AUTHENTICATION: Auth;
|
AUTHENTICATION: Auth;
|
||||||
@ -626,6 +628,9 @@ export class ConfigService {
|
|||||||
EVOAI: {
|
EVOAI: {
|
||||||
ENABLED: process.env?.EVOAI_ENABLED === 'true',
|
ENABLED: process.env?.EVOAI_ENABLED === 'true',
|
||||||
},
|
},
|
||||||
|
FLOWISE: {
|
||||||
|
ENABLED: process.env?.FLOWISE_ENABLED === 'true',
|
||||||
|
},
|
||||||
CACHE: {
|
CACHE: {
|
||||||
REDIS: {
|
REDIS: {
|
||||||
ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true',
|
ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true',
|
||||||
|
Loading…
Reference in New Issue
Block a user