From a299805fee9ebecb6a441af8db888fb30f142c84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Cavalcanti?= Date: Wed, 11 Dec 2024 23:15:54 -0300 Subject: [PATCH] =?UTF-8?q?mudan=C3=A7a=20no=20servi=C3=A7o,=20adi=C3=A7?= =?UTF-8?q?=C3=A3o=20de=20redis,=20interface=20criada?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 10 +- .env.example | 22 ++-- .gitignore | 5 +- Dockerfile | 26 +++-- config.py | 149 ++++++++++++++------------- data/config.json | 7 ++ docker-compose.yaml | 50 +++++---- fluxo.png | Bin 21544 -> 40215 bytes main.py | 166 ++++++++++++++++++++++-------- manager.py | 222 ++++++++++++++++++++++++++++++++++++++++ requirements.txt | 8 +- services.py | 149 +++++++++++++++++++-------- start.sh | 22 ++++ static/fluxo.png | Bin 0 -> 40215 bytes storage.py | 171 +++++++++++++++++++++++++++++++ transcription_logs.json | 0 16 files changed, 807 insertions(+), 200 deletions(-) create mode 100644 data/config.json create mode 100644 manager.py create mode 100644 start.sh create mode 100644 static/fluxo.png create mode 100644 storage.py create mode 100644 transcription_logs.json diff --git a/.dockerignore b/.dockerignore index d0d67da..f53fbf4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,4 +2,12 @@ docker-composer.yaml docker-compose.yaml .gitignore -.git \ No newline at end of file +.git +__pycache__ +*.pyc +.git +.env +.venv +*.md +*.postman_collection.json +deploy_*.sh \ No newline at end of file diff --git a/.env.example b/.env.example index 1ad59e6..0ccdcd6 100644 --- a/.env.example +++ b/.env.example @@ -1,14 +1,12 @@ -# Chave da API para transcrição (Groq ou qualquer outro serviço que você utilizar) -GROQ_API_KEY=substitua_sua_chave_GROQ_aqui +# Debug e Logs +DEBUG_MODE=false +LOG_LEVEL=INFO -# Comportamento da transcrição -PROCESS_SELF_MESSAGES=true -BUSINESS_MESSAGE="substitua_sua_mensagem_de_servico_aqui" -PROCESS_GROUP_MESSAGES=false +# Credenciais do Gerenciador +MANAGER_USER=admin +MANAGER_PASSWORD=impacteai2024 -# Host e porta do Redis (caso esteja utilizando) -REDIS_HOST=localhost -REDIS_PORT=6379 - -DEBUG_MODE=true -LOG_LEVEL=INFO \ No newline at end of file +# Configurações do Servidor +FASTAPI_PORT=8005 +STREAMLIT_PORT=8501 +HOST=0.0.0.0 diff --git a/.gitignore b/.gitignore index ab01a68..5309f25 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ *.pyc docker-composer.yaml GPT.postman_collection.json -.venv/ \ No newline at end of file +.venv/ +.gitignore +deploy_producao.sh +Dockerfile \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f73c40b..1166dcd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,31 @@ # Usar uma imagem oficial do Python como base FROM python:3.10-slim +# Instalar dependências do sistema +RUN apt-get update && apt-get install -y --no-install-recommends \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + # Definir o diretório de trabalho WORKDIR /app -# Copiar o requirements.txt e instalar dependências +# Copiar o arquivo requirements.txt e instalar dependências COPY requirements.txt . - RUN pip install --no-cache-dir -r requirements.txt -# Copiar todo o código para dentro do contêiner +# Copiar todo o código da aplicação COPY . . -# Expor a porta onde o FastAPI vai rodar -EXPOSE 8005 +# Garantir que o diretório static existe +RUN mkdir -p /app/static -# Comando para iniciar a aplicação -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8005"] +# Copiar arquivos estáticos para o diretório apropriado +COPY static/ /app/static/ + +# Garantir permissões de execução ao script inicial +RUN chmod +x start.sh + +# Expor as portas usadas pela aplicação +EXPOSE 8005 8501 + +# Definir o comando inicial +CMD ["./start.sh"] \ No newline at end of file diff --git a/config.py b/config.py index cdb589b..3f5b3eb 100644 --- a/config.py +++ b/config.py @@ -1,102 +1,111 @@ -import os -from dotenv import load_dotenv import logging -from pathlib import Path +import redis +import os # Configuração de logging com cores e formatação melhorada class ColoredFormatter(logging.Formatter): - """Formatter personalizado que adiciona cores aos logs""" - grey = "\x1b[38;21m" - blue = "\x1b[38;5;39m" - yellow = "\x1b[38;5;226m" - red = "\x1b[38;5;196m" - bold_red = "\x1b[31;1m" - reset = "\x1b[0m" - - def __init__(self, fmt): - super().__init__() - self.fmt = fmt - self.FORMATS = { - logging.DEBUG: self.blue + self.fmt + self.reset, - logging.INFO: self.grey + self.fmt + self.reset, - logging.WARNING: self.yellow + self.fmt + self.reset, - logging.ERROR: self.red + self.fmt + self.reset, - logging.CRITICAL: self.bold_red + self.fmt + self.reset - } + """Formatter personalizado que adiciona cores aos logs.""" + COLORS = { + logging.DEBUG: "\x1b[38;5;39m", # Azul + logging.INFO: "\x1b[38;21m", # Cinza + logging.WARNING: "\x1b[38;5;226m", # Amarelo + logging.ERROR: "\x1b[38;5;196m", # Vermelho + logging.CRITICAL: "\x1b[31;1m", # Vermelho forte + } + RESET = "\x1b[0m" def format(self, record): - log_fmt = self.FORMATS.get(record.levelno) + color = self.COLORS.get(record.levelno, self.RESET) + log_fmt = f"{color}%(asctime)s - %(name)s - %(levelname)s - %(message)s{self.RESET}" formatter = logging.Formatter(log_fmt) return formatter.format(record) # Configuração inicial do logging -logger = logging.getLogger(__name__) +logger = logging.getLogger("TranscreveZAP") handler = logging.StreamHandler() -handler.setFormatter(ColoredFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) +handler.setFormatter(ColoredFormatter()) logger.addHandler(handler) -# Carregar variáveis de ambiente -env_path = Path('.env') -if env_path.exists(): - logger.debug(f"Arquivo .env encontrado em: {env_path.absolute()}") - load_dotenv(override=True) -else: - logger.warning("Arquivo .env não encontrado! Usando variáveis de ambiente do sistema.") +# Nível de log inicial (pode ser ajustado após o carregamento de configurações) +logger.setLevel(logging.INFO) + +# Conexão com o Redis +redis_client = redis.Redis( + host=os.getenv('REDIS_HOST', 'localhost'), + port=int(os.getenv('REDIS_PORT', 6380)), + db=0, + decode_responses=True +) class Settings: + """Classe para gerenciar configurações do sistema.""" def __init__(self): - logger.debug("Iniciando carregamento das configurações...") - - # Carregamento das variáveis com logs detalhados - self.DEBUG_MODE = os.getenv('DEBUG_MODE', 'false').lower() == 'true' - logger.debug(f"DEBUG_MODE configurado como: {self.DEBUG_MODE}") - - self.GROQ_API_KEY = os.getenv('GROQ_API_KEY') + """Inicializa as configurações.""" + logger.debug("Carregando configurações do Redis...") + + self.GROQ_API_KEY = self.get_redis_value("GROQ_API_KEY", "gsk_default_key") + self.BUSINESS_MESSAGE = self.get_redis_value("BUSINESS_MESSAGE", "*Impacte AI* Premium Services") + self.PROCESS_GROUP_MESSAGES = self.get_redis_value("PROCESS_GROUP_MESSAGES", "false").lower() == "true" + self.PROCESS_SELF_MESSAGES = self.get_redis_value("PROCESS_SELF_MESSAGES", "true").lower() == "true" + self.LOG_LEVEL = self.get_redis_value("LOG_LEVEL", "INFO").upper() + + # Mascarar chave ao logar if self.GROQ_API_KEY: masked_key = f"{self.GROQ_API_KEY[:10]}...{self.GROQ_API_KEY[-4:]}" logger.debug(f"GROQ_API_KEY carregada: {masked_key}") else: logger.error("GROQ_API_KEY não encontrada!") - - self.BUSINESS_MESSAGE = os.getenv('BUSINESS_MESSAGE', '*Impacte AI* Premium Services') - logger.debug(f"BUSINESS_MESSAGE configurada como: {self.BUSINESS_MESSAGE}") - - self.PROCESS_GROUP_MESSAGES = os.getenv('PROCESS_GROUP_MESSAGES', 'false').lower() == 'true' - logger.debug(f"PROCESS_GROUP_MESSAGES configurado como: {self.PROCESS_GROUP_MESSAGES}") - - self.PROCESS_SELF_MESSAGES = os.getenv('PROCESS_SELF_MESSAGES', 'false').lower() == 'true' - logger.debug(f"PROCESS_SELF_MESSAGES configurado como: {self.PROCESS_SELF_MESSAGES}") - self.LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') - logger.debug(f"LOG_LEVEL configurado como: {self.LOG_LEVEL}") + logger.debug( + f"Configurações carregadas: LOG_LEVEL={self.LOG_LEVEL}, " + f"PROCESS_GROUP_MESSAGES={self.PROCESS_GROUP_MESSAGES}, " + f"PROCESS_SELF_MESSAGES={self.PROCESS_SELF_MESSAGES}" + ) + + def get_redis_value(self, key, default): + """Obtém um valor do Redis com fallback para o valor padrão.""" + value = redis_client.get(key) + if value is None: + logger.warning(f"Configuração '{key}' não encontrada no Redis. Usando padrão: {default}") + return default + return value + + def set_redis_value(self, key, value): + """Define um valor no Redis.""" + redis_client.set(key, value) + logger.debug(f"Configuração '{key}' atualizada no Redis") def validate(self): - """Validação detalhada das configurações críticas""" - logger.debug("Iniciando validação das configurações...") - - validation_errors = [] - - if not self.GROQ_API_KEY: - validation_errors.append("GROQ_API_KEY não está definida") - elif not self.GROQ_API_KEY.startswith('gsk_'): - validation_errors.append("GROQ_API_KEY inválida: deve começar com 'gsk_'") + """Validação detalhada das configurações críticas.""" + logger.debug("Validando configurações...") + errors = [] - if validation_errors: - for error in validation_errors: - logger.error(f"Erro de validação: {error}") + if not self.GROQ_API_KEY: + errors.append("GROQ_API_KEY não está definida.") + elif not self.GROQ_API_KEY.startswith("gsk_"): + errors.append("GROQ_API_KEY inválida: deve começar com 'gsk_'.") + + if errors: + for error in errors: + logger.error(error) return False - + logger.info("Todas as configurações foram validadas com sucesso!") return True -# Criar instância das configurações +# Instância única de configurações settings = Settings() - -# Validar configurações if not settings.validate(): - logger.critical("Configurações inválidas detectadas. A aplicação pode não funcionar corretamente!") + logger.critical("Configurações inválidas detectadas durante a inicialização.") + settings = None # Evita que seja referenciado como 'NoneType' -# Ajustar nível de log -log_level = logging.DEBUG if settings.DEBUG_MODE else getattr(logging, settings.LOG_LEVEL.upper()) -logger.setLevel(log_level) -logger.info(f"Nível de log definido como: {logging.getLevelName(log_level)}") \ No newline at end of file +def load_settings(): + """ + Recarrega as configurações do Redis. + """ + global settings + settings = Settings() + # Ajustar nível de log + log_level = getattr(logging, settings.LOG_LEVEL, logging.INFO) + logger.setLevel(log_level) + logger.info(f"Nível de log ajustado para: {logging.getLevelName(log_level)}") \ No newline at end of file diff --git a/data/config.json b/data/config.json new file mode 100644 index 0000000..1db4415 --- /dev/null +++ b/data/config.json @@ -0,0 +1,7 @@ +{ + "GROQ_API_KEY": "default_key", + "BUSINESS_MESSAGE": "*Impacte AI* Premium Services", + "PROCESS_GROUP_MESSAGES": false, + "PROCESS_SELF_MESSAGES": true, + "DEBUG_MODE": false +} diff --git a/docker-compose.yaml b/docker-compose.yaml index d89d8f4..1a0c12c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,24 +1,32 @@ version: "3.7" services: - transcricaoaudio: + tcaudio: image: impacteai/transcrevezap:latest build: . networks: - - suarededocker #troque pela sua rede do docker + - suarededocker ports: - - 8005:8005 + - 8005:8005 # FastAPI + - 8501:8501 # Streamlit environment: Uvicorn_port: 8005 Uvicorn_host: 0.0.0.0 Uvicorn_reload: "true" Uvicorn_workers: 1 - GROQ_API_KEY: "substitua_sua_chave_GROQ_aqui" #coloque sua chave GROQ aqui - BUSINESS_MESSAGE: "substitua_sua_mensagem_de_servico_aqui" #coloque a mensagem que será enviada ao final da transcrição aqui - PROCESS_GROUP_MESSAGES: "false" # Define se mensagens de grupos devem ser processadas - PROCESS_SELF_MESSAGES: "true" # Define se sua próprias mensagens devem ser processadas + GROQ_API_KEY: "${GROQ_API_KEY}" + BUSINESS_MESSAGE: "*Impacte AI* Premium Services" + PROCESS_GROUP_MESSAGES: "false" + PROCESS_SELF_MESSAGES: "true" DEBUG_MODE: "false" LOG_LEVEL: "INFO" + MANAGER_USER: "admin" + MANAGER_PASSWORD: "impacte2024" + volumes: + - ./config.json:/app/config.json + - ./backups:/app/backups + - ./transcription_logs.json:/app/transcription_logs.json + - ./static:/app/static deploy: mode: replicated replicas: 1 @@ -27,20 +35,22 @@ services: - node.role == manager labels: - traefik.enable=true - - traefik.http.routers.transcricaoaudio.rule=Host(`transcricaoaudio.seudominio.com.br`) #coloque seu subdominio apontado aqui - - traefik.http.routers.transcricaoaudio.entrypoints=websecure - - traefik.http.routers.transcricaoaudio.tls.certresolver=letsencryptresolver - - traefik.http.services.transcricaoaudio.loadbalancer.server.port=8005 - - traefik.http.services.transcricaoaudio.loadbalancer.passHostHeader=true - - traefik.http.routers.transcricaoaudio.service=transcricaoaudio + - traefik.http.routers.tcaudio.rule=Host(`transcrevezap.seudominio.com.br`) + - traefik.http.routers.tcaudio.entrypoints=websecure + - traefik.http.routers.tcaudio.tls.certresolver=letsencryptresolver + - traefik.http.services.tcaudio.loadbalancer.server.port=8005 + - traefik.http.services.tcaudio.loadbalancer.passHostHeader=true + - traefik.http.routers.tcaudio.service=tcaudio - traefik.http.middlewares.traefik-compress.compress=true - - traefik.http.routers.transcricaoaudio.middlewares=traefik-compress - resources: - limits: - cpus: "1" - memory: 1024M + - traefik.http.routers.tcaudio.middlewares=traefik-compress + # Configuração do Streamlit + - traefik.http.routers.tcaudio-manager.rule=Host(`manager.transcrevezap.seudominio.com.br`) + - traefik.http.routers.tcaudio-manager.entrypoints=websecure + - traefik.http.routers.tcaudio-manager.tls.certresolver=letsencryptresolver + - traefik.http.services.tcaudio-manager.loadbalancer.server.port=8501 + - traefik.http.routers.tcaudio-manager.service=tcaudio-manager networks: - suarededocker: #troque pela sua rede do docker + suarededocker: external: true - name: suarededocker #troque pela sua rede do docker \ No newline at end of file + name: suarededocker diff --git a/fluxo.png b/fluxo.png index 98af4809ccdb393ae55e60dd5242783b8f54a5c2..12905cdaac7f70eafe49c4efc2b53d6ef2402284 100644 GIT binary patch literal 40215 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 literal 21544 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 diff --git a/main.py b/main.py index 8d38037..7771df9 100644 --- a/main.py +++ b/main.py @@ -7,19 +7,52 @@ from services import ( summarize_text_if_needed, ) from models import WebhookRequest -import aiohttp -from config import settings, logger +from config import logger, settings, redis_client +from storage import StorageHandler +import traceback +import os app = FastAPI() +storage = StorageHandler() + +# Função para buscar configurações do Redis com fallback para valores padrão +def get_config(key, default=None): + try: + value = redis_client.get(key) + if value is None: + logger.warning(f"Configuração '{key}' não encontrada no Redis. Usando padrão: {default}") + return default + return value + except Exception as e: + logger.error(f"Erro ao acessar Redis: {e}") + return default + +# Carregando configurações dinâmicas do Redis +def load_dynamic_settings(): + return { + "GROQ_API_KEY": get_config("GROQ_API_KEY", "default_key"), + "BUSINESS_MESSAGE": get_config("BUSINESS_MESSAGE", "*Impacte AI* Premium Services"), + "PROCESS_GROUP_MESSAGES": get_config("PROCESS_GROUP_MESSAGES", "false") == "true", + "PROCESS_SELF_MESSAGES": get_config("PROCESS_SELF_MESSAGES", "true") == "true", + "DEBUG_MODE": get_config("DEBUG_MODE", "false") == "true", + } @app.post("/transcreve-audios") async def transcreve_audios(request: Request): try: - logger.info("Iniciando processamento de áudio") body = await request.json() + dynamic_settings = load_dynamic_settings() - if settings.DEBUG_MODE: - logger.debug(f"Payload recebido: {body}") + # Log inicial da requisição + storage.add_log("INFO", "Nova requisição de transcrição recebida", { + "instance": body.get("instance"), + "event": body.get("event") + }) + + if dynamic_settings["DEBUG_MODE"]: + storage.add_log("DEBUG", "Payload completo recebido", { + "body": body + }) # Extraindo informações server_url = body["server_url"] @@ -30,58 +63,101 @@ async def transcreve_audios(request: Request): remote_jid = body["data"]["key"]["remoteJid"] message_type = body["data"]["messageType"] + # Verificação de tipo de mensagem if "audioMessage" not in message_type: - logger.info("Mensagem recebida não é um áudio, ignorando") + storage.add_log("INFO", "Mensagem ignorada - não é áudio", { + "message_type": message_type, + "remote_jid": remote_jid + }) return {"message": "Mensagem recebida não é um áudio"} - if from_me and not settings.PROCESS_SELF_MESSAGES: - logger.info("Mensagem enviada pelo próprio usuário ignorada conforme configuração") + # Verificação de permissões + if not storage.can_process_message(remote_jid): + is_group = "@g.us" in remote_jid + storage.add_log("INFO", + "Mensagem não autorizada para processamento", + { + "remote_jid": remote_jid, + "tipo": "grupo" if is_group else "usuário", + "motivo": "grupo não permitido" if is_group else "usuário bloqueado" + } + ) + return {"message": "Mensagem não autorizada para processamento"} + + if from_me and not dynamic_settings["PROCESS_SELF_MESSAGES"]: + storage.add_log("INFO", "Mensagem própria ignorada", { + "remote_jid": remote_jid + }) return {"message": "Mensagem enviada por mim, sem operação"} - if "@g.us" in remote_jid and not settings.PROCESS_GROUP_MESSAGES: - logger.info("Mensagem de grupo ignorada conforme configuração") - return {"message": "Mensagem enviada por um grupo, sem operação"} + # Obter áudio + try: + if "mediaUrl" in body["data"]["message"]: + audio_source = body["data"]["message"]["mediaUrl"] + storage.add_log("DEBUG", "Usando mediaUrl para áudio", { + "mediaUrl": audio_source + }) + else: + storage.add_log("DEBUG", "Obtendo áudio via base64") + base64_audio = await get_audio_base64(server_url, instance, apikey, audio_key) + audio_source = await convert_base64_to_file(base64_audio) + storage.add_log("DEBUG", "Áudio convertido", { + "source": audio_source + }) - # Verificar se temos mediaUrl ou precisamos pegar o base64 - if "mediaUrl" in body["data"]["message"]: - audio_source = body["data"]["message"]["mediaUrl"] - logger.debug(f"Usando mediaUrl: {audio_source}") - else: - logger.debug("MediaUrl não encontrada, obtendo áudio via base64") - base64_audio = await get_audio_base64(server_url, instance, apikey, audio_key) - audio_source = await convert_base64_to_file(base64_audio) - logger.debug(f"Áudio convertido e salvo em: {audio_source}") + # Transcrever áudio + storage.add_log("INFO", "Iniciando transcrição") + transcription_text, _ = await transcribe_audio(audio_source) + + # Resumir se necessário + summary_text = await summarize_text_if_needed(transcription_text) + + # Formatar mensagem + summary_message = ( + f"🤖 *Resumo do áudio:*\n\n" + f"{summary_text}\n\n" + f"🔊 *Transcrição do áudio:*\n\n" + f"{transcription_text}\n\n" + f"{dynamic_settings['BUSINESS_MESSAGE']}" + ) - # Transcrever o áudio - transcription_text, _ = await transcribe_audio(audio_source) - summary_text = await summarize_text_if_needed(transcription_text) + # Enviar resposta + await send_message_to_whatsapp( + server_url, + instance, + apikey, + summary_message, + remote_jid, + audio_key, + ) - # Formatar a mensagem - summary_message = ( - f"🤖 *Resumo do áudio:*\n\n" - f"{summary_text}\n\n" - f"🔊 *Transcrição do áudio:*\n\n" - f"{transcription_text}\n\n" - f"{settings.BUSINESS_MESSAGE}" - ) - logger.debug(f"Mensagem formatada: {summary_message[:100]}...") + # Registrar sucesso + storage.record_processing(remote_jid) + storage.add_log("INFO", "Áudio processado com sucesso", { + "remote_jid": remote_jid, + "transcription_length": len(transcription_text), + "summary_length": len(summary_text) + }) - # Enviar a mensagem formatada via WhatsApp - await send_message_to_whatsapp( - server_url, - instance, - apikey, - summary_message, - remote_jid, - audio_key, - ) + return {"message": "Áudio transcrito e resposta enviada com sucesso"} - logger.info("Áudio processado e resposta enviada com sucesso") - return {"message": "Áudio transcrito e resposta enviada com sucesso"} + except Exception as e: + storage.add_log("ERROR", f"Erro ao processar áudio: {str(e)}", { + "error_type": type(e).__name__, + "remote_jid": remote_jid, + "traceback": traceback.format_exc() + }) + raise HTTPException( + status_code=500, + detail=f"Erro ao processar áudio: {str(e)}" + ) except Exception as e: - logger.error(f"Erro ao processar áudio: {str(e)}", exc_info=settings.DEBUG_MODE) + storage.add_log("ERROR", f"Erro na requisição: {str(e)}", { + "error_type": type(e).__name__, + "traceback": traceback.format_exc() + }) raise HTTPException( status_code=500, - detail=f"Erro ao processar a requisição: {str(e)}", + detail=f"Erro ao processar a requisição: {str(e)}" ) \ No newline at end of file diff --git a/manager.py b/manager.py new file mode 100644 index 0000000..c445085 --- /dev/null +++ b/manager.py @@ -0,0 +1,222 @@ +import streamlit as st +import pandas as pd +from datetime import datetime +from storage import StorageHandler +import plotly.express as px +import os +import redis + +# Conectar ao Redis +redis_client = redis.Redis(host=os.getenv('REDIS_HOST', 'localhost'), port=int(os.getenv('REDIS_PORT', 6380)), decode_responses=True) + +# Função para salvar configurações no Redis +def save_to_redis(key, value): + try: + redis_client.set(key, value) + st.success(f"Configuração {key} salva com sucesso!") + except Exception as e: + st.error(f"Erro ao salvar no Redis: {key} -> {e}") + +# Função para buscar configurações no Redis +def get_from_redis(key, default=None): + try: + value = redis_client.get(key) + return value if value is not None else default + except Exception as e: + st.error(f"Erro ao buscar no Redis: {key} -> {e}") + return default + +# Configuração da página +st.set_page_config( + page_title="TranscreveZAP by Impacte AI", + page_icon="🎙️", + layout="wide", + initial_sidebar_state="expanded", +) + +# Configuração do storage +storage = StorageHandler() + +# Função para carregar configurações do Redis para o Streamlit +def load_settings(): + try: + st.session_state.settings = { + "GROQ_API_KEY": get_from_redis("GROQ_API_KEY", "default_key"), + "BUSINESS_MESSAGE": get_from_redis("BUSINESS_MESSAGE", "*Impacte AI* Premium Services"), + "PROCESS_GROUP_MESSAGES": get_from_redis("PROCESS_GROUP_MESSAGES", "false"), + "PROCESS_SELF_MESSAGES": get_from_redis("PROCESS_SELF_MESSAGES", "true"), + } + except Exception as e: + st.error(f"Erro ao carregar configurações do Redis: {e}") + +# Carregar configurações na sessão, se necessário +if "settings" not in st.session_state: + load_settings() + +# Função para salvar configurações do Streamlit no Redis +def save_settings(): + try: + save_to_redis("GROQ_API_KEY", st.session_state.groq_api_key) + save_to_redis("BUSINESS_MESSAGE", st.session_state.business_message) + save_to_redis("PROCESS_GROUP_MESSAGES", st.session_state.process_group_messages) + save_to_redis("PROCESS_SELF_MESSAGES", st.session_state.process_self_messages) + st.success("Configurações salvas com sucesso!") + except Exception as e: + st.error(f"Erro ao salvar configurações: {e}") + +def show_logo(): + try: + logo_path = os.path.join(os.path.dirname(__file__), "static", "fluxo.png") + if os.path.exists(logo_path): + st.image(logo_path, width=150) + else: + st.warning("Logo não encontrada.") + except Exception as e: + st.error(f"Erro ao carregar logo: {e}") + +def show_footer(): + st.markdown( + """ + + """, + unsafe_allow_html=True, + ) + +def login_page(): + show_logo() + col1, col2, col3 = st.columns([1, 2, 1]) + with col2: + with st.form("login_form"): + st.markdown("

Login

", unsafe_allow_html=True) + username = st.text_input('Usuário') + password = st.text_input('Senha', type='password') + if st.form_submit_button('Entrar', use_container_width=True): + if username == os.getenv('MANAGER_USER') and password == os.getenv('MANAGER_PASSWORD'): + st.session_state.authenticated = True + st.experimental_rerun() + else: + st.error('Credenciais inválidas') + +def dashboard(): + show_logo() + st.sidebar.markdown('', unsafe_allow_html=True) + page = st.sidebar.radio( + "Navegação", + ["📊 Painel de Controle", "👥 Gerenciar Grupos", "🚫 Gerenciar Bloqueios", "⚙️ Configurações"] + ) + if st.sidebar.button("Sair"): + st.session_state.authenticated = False + st.experimental_rerun() + + if page == "📊 Painel de Controle": + show_statistics() + elif page == "👥 Gerenciar Grupos": + manage_groups() + elif page == "🚫 Gerenciar Bloqueios": + manage_blocks() + elif page == "⚙️ Configurações": + manage_settings() + +def show_statistics(): + st.title("📊 Painel de Controle") + try: + stats = storage.get_statistics() + col1, col2, col3 = st.columns(3) + with col1: + st.metric("Total de Áudios Processados", stats.get("total_processed", 0)) + with col2: + last_processed = stats.get("last_processed", "Nunca") + st.metric("Último Processamento", last_processed) + with col3: + total_groups = len(storage.get_allowed_groups()) + st.metric("Grupos Permitidos", total_groups) + + daily_data = stats["stats"]["daily_count"] + if daily_data: + df = pd.DataFrame(list(daily_data.items()), columns=['Data', 'Processamentos']) + df['Data'] = pd.to_datetime(df['Data']) + fig = px.line(df, x='Data', y='Processamentos', title='Processamentos por Dia') + st.plotly_chart(fig, use_container_width=True) + else: + st.info("Ainda não há dados de processamento disponíveis.") + except Exception as e: + st.error(f"Erro ao carregar estatísticas: {e}") + +def manage_groups(): + st.title("👥 Gerenciar Grupos") + st.subheader("Adicionar Grupo Permitido") + col1, col2 = st.columns([3, 1]) + with col1: + new_group = st.text_input("Número do Grupo", placeholder="Ex: 5521999999999") + with col2: + if st.button("Adicionar"): + formatted_group = f"{new_group}@g.us" + storage.add_allowed_group(formatted_group) + st.success(f"Grupo {formatted_group} adicionado com sucesso!") + st.experimental_rerun() + + st.subheader("Grupos Permitidos") + allowed_groups = storage.get_allowed_groups() + if allowed_groups: + for group in allowed_groups: + col1, col2 = st.columns([4, 1]) + with col1: + st.text(group) + with col2: + if st.button("Remover", key=f"remove_{group}"): + storage.remove_allowed_group(group) + st.success(f"Grupo {group} removido!") + st.experimental_rerun() + else: + st.info("Nenhum grupo permitido.") + +def manage_blocks(): + st.title("🚫 Gerenciar Bloqueios") + st.subheader("Bloquear Usuário") + col1, col2 = st.columns([3, 1]) + with col1: + new_user = st.text_input("Número do Usuário", placeholder="Ex: 5521999999999") + with col2: + if st.button("Bloquear"): + formatted_user = f"{new_user}@s.whatsapp.net" + storage.add_blocked_user(formatted_user) + st.success(f"Usuário {formatted_user} bloqueado!") + st.experimental_rerun() + + st.subheader("Usuários Bloqueados") + blocked_users = storage.get_blocked_users() + if blocked_users: + for user in blocked_users: + col1, col2 = st.columns([4, 1]) + with col1: + st.text(user) + with col2: + if st.button("Desbloquear", key=f"unblock_{user}"): + storage.remove_blocked_user(user) + st.success(f"Usuário {user} desbloqueado!") + st.experimental_rerun() + else: + st.info("Nenhum usuário bloqueado.") + +def manage_settings(): + st.title("⚙️ Configurações") + st.subheader("Configurações do Sistema") + st.text_input("GROQ_API_KEY", value=st.session_state.settings["GROQ_API_KEY"], key="groq_api_key") + st.text_input("Mensagem de Boas-Vindas", value=st.session_state.settings["BUSINESS_MESSAGE"], key="business_message") + st.selectbox("Processar Mensagens em Grupos", options=["true", "false"], index=["true", "false"].index(st.session_state.settings["PROCESS_GROUP_MESSAGES"]), key="process_group_messages") + st.selectbox("Processar Mensagens Próprias", options=["true", "false"], index=["true", "false"].index(st.session_state.settings["PROCESS_SELF_MESSAGES"]), key="process_self_messages") + if st.button("Salvar Configurações"): + save_settings() + +if "authenticated" not in st.session_state: + st.session_state.authenticated = False + +if st.session_state.authenticated: + dashboard() +else: + login_page() + +show_footer() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ceb0554..f989f43 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,21 +8,23 @@ attrs==24.2.0 certifi==2024.8.30 charset-normalizer==2.1.1 click==8.1.7 -fastapi==0.109.2 +fastapi==0.115.6 frozenlist==1.4.1 h11==0.14.0 idna==3.10 multidict==6.1.0 pip-system-certs==4.0 +plotly==5.18.0 propcache==0.2.0 pydantic==2.10.3 pydantic-settings==2.6.1 python-dotenv==1.0.1 requests==2.28.1 sniffio==1.3.1 -starlette>=0.36.3,<0.37.0 +streamlit==1.31.0 typing_extensions==4.12.2 urllib3==1.26.20 -uvicorn==0.17.6 +uvicorn==0.23.2 wrapt==1.16.0 yarl==1.15.2 +redis \ No newline at end of file diff --git a/services.py b/services.py index b0fbcee..4a04ff1 100644 --- a/services.py +++ b/services.py @@ -2,27 +2,40 @@ import aiohttp import base64 import aiofiles from fastapi import HTTPException -from config import settings, logger +from config import settings, logger, redis_client +from storage import StorageHandler +import os +import json +import tempfile + +# Inicializa o storage handler +storage = StorageHandler() async def convert_base64_to_file(base64_data): """Converte dados base64 em arquivo temporário""" try: - logger.debug("Iniciando conversão de base64 para arquivo") + storage.add_log("DEBUG", "Iniciando conversão de base64 para arquivo") audio_data = base64.b64decode(base64_data) - audio_file_path = "/tmp/audio_file.mp3" - - async with aiofiles.open(audio_file_path, "wb") as f: - await f.write(audio_data) + with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file: + temp_file.write(audio_data) + audio_file_path = temp_file.name - logger.debug(f"Arquivo temporário criado em: {audio_file_path}") + storage.add_log("DEBUG", "Arquivo temporário criado", { + "path": audio_file_path + }) return audio_file_path except Exception as e: - logger.error(f"Erro na conversão base64: {str(e)}", exc_info=settings.DEBUG_MODE) + storage.add_log("ERROR", "Erro na conversão base64", { + "error": str(e), + "type": type(e).__name__ + }) raise async def summarize_text_if_needed(text): """Resumir texto usando a API GROQ""" - logger.debug("Iniciando processo de resumo do texto") + storage.add_log("DEBUG", "Iniciando processo de resumo", { + "text_length": len(text) + }) url_completions = "https://api.groq.com/openai/v1/chat/completions" headers = { @@ -50,25 +63,33 @@ async def summarize_text_if_needed(text): try: async with aiohttp.ClientSession() as session: - logger.debug("Enviando requisição para API GROQ") + storage.add_log("DEBUG", "Enviando requisição para API GROQ") async with session.post(url_completions, headers=headers, json=json_data) as summary_response: if summary_response.status == 200: summary_result = await summary_response.json() summary_text = summary_result["choices"][0]["message"]["content"] - logger.info("Resumo gerado com sucesso") - logger.debug(f"Resumo: {summary_text[:100]}...") + storage.add_log("INFO", "Resumo gerado com sucesso", { + "original_length": len(text), + "summary_length": len(summary_text) + }) return summary_text else: error_text = await summary_response.text() - logger.error(f"Erro na API GROQ: {error_text}") + storage.add_log("ERROR", "Erro na API GROQ", { + "error": error_text, + "status": summary_response.status + }) raise Exception(f"Erro ao resumir o texto: {error_text}") except Exception as e: - logger.error(f"Erro no processo de resumo: {str(e)}", exc_info=settings.DEBUG_MODE) + storage.add_log("ERROR", "Erro no processo de resumo", { + "error": str(e), + "type": type(e).__name__ + }) raise async def transcribe_audio(audio_source, apikey=None): """Transcreve áudio usando a API GROQ""" - logger.info("Iniciando processo de transcrição") + storage.add_log("INFO", "Iniciando processo de transcrição") url = "https://api.groq.com/openai/v1/audio/transcriptions" groq_headers = {"Authorization": f"Bearer {settings.GROQ_API_KEY}"} @@ -76,21 +97,27 @@ async def transcribe_audio(audio_source, apikey=None): async with aiohttp.ClientSession() as session: # Se o audio_source for uma URL if isinstance(audio_source, str) and audio_source.startswith('http'): - logger.debug(f"Baixando áudio da URL: {audio_source}") + storage.add_log("DEBUG", "Baixando áudio da URL", { + "url": audio_source + }) download_headers = {"apikey": apikey} if apikey else {} async with session.get(audio_source, headers=download_headers) as response: if response.status != 200: error_text = await response.text() - logger.error(f"Erro no download do áudio: Status {response.status}, Resposta: {error_text}") + storage.add_log("ERROR", "Erro no download do áudio", { + "status": response.status, + "error": error_text + }) raise Exception(f"Erro ao baixar áudio: {error_text}") audio_data = await response.read() - temp_file = "/tmp/audio_from_url.mp3" - async with aiofiles.open(temp_file, "wb") as f: - await f.write(audio_data) - audio_source = temp_file - logger.debug(f"Áudio salvo temporariamente em: {temp_file}") + with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file: + temp_file.write(audio_data) + audio_source = temp_file.name + storage.add_log("DEBUG", "Áudio salvo temporariamente", { + "path": audio_source + }) # Preparar dados para transcrição data = aiohttp.FormData() @@ -98,51 +125,73 @@ async def transcribe_audio(audio_source, apikey=None): data.add_field('model', 'whisper-large-v3') data.add_field('language', 'pt') - logger.debug("Enviando áudio para transcrição") + storage.add_log("DEBUG", "Enviando áudio para transcrição") async with session.post(url, headers=groq_headers, data=data) as response: if response.status == 200: result = await response.json() message = result.get("text", "") - logger.info("Transcrição concluída com sucesso") - logger.debug(f"Texto transcrito: {message[:100]}...") + storage.add_log("INFO", "Transcrição concluída com sucesso", { + "text_length": len(message) + }) is_summary = False if len(message) > 1000: - logger.debug("Texto longo detectado, iniciando resumo") + storage.add_log("DEBUG", "Texto longo detectado, iniciando resumo", { + "text_length": len(message) + }) is_summary = True message = await summarize_text_if_needed(message) return message, is_summary else: error_text = await response.text() - logger.error(f"Erro na transcrição: {error_text}") + storage.add_log("ERROR", "Erro na transcrição", { + "error": error_text, + "status": response.status + }) raise Exception(f"Erro na transcrição: {error_text}") except Exception as e: - logger.error(f"Erro no processo de transcrição: {str(e)}", exc_info=settings.DEBUG_MODE) + storage.add_log("ERROR", "Erro no processo de transcrição", { + "error": str(e), + "type": type(e).__name__ + }) raise + finally: + # Limpar arquivos temporários + if isinstance(audio_source, str) and os.path.exists(audio_source): + os.unlink(audio_source) async def send_message_to_whatsapp(server_url, instance, apikey, message, remote_jid, message_id): """Envia mensagem via WhatsApp""" - logger.debug(f"Preparando envio de mensagem para: {remote_jid}") + storage.add_log("DEBUG", "Preparando envio de mensagem", { + "remote_jid": remote_jid, + "instance": instance + }) url = f"{server_url}/message/sendText/{instance}" headers = {"apikey": apikey} try: # Tentar enviar na V1 body = get_body_message_to_whatsapp_v1(message, remote_jid) - logger.debug("Tentando envio no formato V1") + storage.add_log("DEBUG", "Tentando envio no formato V1") result = await call_whatsapp(url, body, headers) # Se falhar, tenta V2 if not result: - logger.debug("Formato V1 falhou, tentando formato V2") + storage.add_log("DEBUG", "Formato V1 falhou, tentando formato V2") body = get_body_message_to_whatsapp_v2(message, remote_jid, message_id) await call_whatsapp(url, body, headers) - logger.info("Mensagem enviada com sucesso") + storage.add_log("INFO", "Mensagem enviada com sucesso", { + "remote_jid": remote_jid + }) except Exception as e: - logger.error(f"Erro no envio da mensagem: {str(e)}", exc_info=settings.DEBUG_MODE) + storage.add_log("ERROR", "Erro no envio da mensagem", { + "error": str(e), + "type": type(e).__name__, + "remote_jid": remote_jid + }) raise def get_body_message_to_whatsapp_v1(message, remote_jid): @@ -165,21 +214,32 @@ async def call_whatsapp(url, body, headers): """Realiza chamada à API do WhatsApp""" try: async with aiohttp.ClientSession() as session: - logger.debug(f"Enviando requisição para: {url}") + storage.add_log("DEBUG", "Enviando requisição para WhatsApp", { + "url": url + }) async with session.post(url, json=body, headers=headers) as response: if response.status not in [200, 201]: error_text = await response.text() - logger.error(f"Erro na API do WhatsApp: Status {response.status}, Resposta: {error_text}") + storage.add_log("ERROR", "Erro na API do WhatsApp", { + "status": response.status, + "error": error_text + }) return False - logger.debug("Requisição bem-sucedida") + storage.add_log("DEBUG", "Requisição bem-sucedida") return True except Exception as e: - logger.error(f"Erro na chamada WhatsApp: {str(e)}", exc_info=settings.DEBUG_MODE) + storage.add_log("ERROR", "Erro na chamada WhatsApp", { + "error": str(e), + "type": type(e).__name__ + }) return False async def get_audio_base64(server_url, instance, apikey, message_id): """Obtém áudio em Base64 via API do WhatsApp""" - logger.debug(f"Obtendo áudio base64 para mensagem: {message_id}") + storage.add_log("DEBUG", "Obtendo áudio base64", { + "message_id": message_id, + "instance": instance + }) url = f"{server_url}/chat/getBase64FromMediaMessage/{instance}" headers = {"apikey": apikey} body = {"message": {"key": {"id": message_id}}, "convertToMp4": False} @@ -189,12 +249,19 @@ async def get_audio_base64(server_url, instance, apikey, message_id): async with session.post(url, json=body, headers=headers) as response: if response.status in [200, 201]: result = await response.json() - logger.info("Áudio base64 obtido com sucesso") + storage.add_log("INFO", "Áudio base64 obtido com sucesso") return result.get("base64", "") else: error_text = await response.text() - logger.error(f"Erro ao obter áudio base64: {error_text}") + storage.add_log("ERROR", "Erro ao obter áudio base64", { + "status": response.status, + "error": error_text + }) raise HTTPException(status_code=500, detail="Falha ao obter áudio em base64") except Exception as e: - logger.error(f"Erro na obtenção do áudio base64: {str(e)}", exc_info=settings.DEBUG_MODE) + storage.add_log("ERROR", "Erro na obtenção do áudio base64", { + "error": str(e), + "type": type(e).__name__, + "message_id": message_id + }) raise \ No newline at end of file diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..e37f6ab --- /dev/null +++ b/start.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Função para inicializar configurações no Redis +initialize_redis_config() { + redis-cli -h $REDIS_HOST -p $REDIS_PORT SET GROQ_API_KEY "sua_api_key_aqui" + redis-cli -h $REDIS_HOST -p $REDIS_PORT SET BUSINESS_MESSAGE "*Impacte AI* Premium Services" + redis-cli -h $REDIS_HOST -p $REDIS_PORT SET PROCESS_GROUP_MESSAGES "false" + redis-cli -h $REDIS_HOST -p $REDIS_PORT SET PROCESS_SELF_MESSAGES "true" + redis-cli -h $REDIS_HOST -p $REDIS_PORT SET DEBUG_MODE "false" +} + +# Inicializar configurações no Redis +initialize_redis_config + +# Iniciar o FastAPI em background +uvicorn main:app --host 0.0.0.0 --port 8005 & + +# Iniciar o Streamlit +streamlit run manager.py --server.address 0.0.0.0 --server.port 8501 + +# Manter o script rodando +wait \ No newline at end of file diff --git a/static/fluxo.png b/static/fluxo.png new file mode 100644 index 0000000000000000000000000000000000000000..12905cdaac7f70eafe49c4efc2b53d6ef2402284 GIT binary patch literal 40215 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 literal 0 HcmV?d00001 diff --git a/storage.py b/storage.py new file mode 100644 index 0000000..862caf6 --- /dev/null +++ b/storage.py @@ -0,0 +1,171 @@ +import json +import os +from typing import List, Dict +from datetime import datetime, timedelta +import traceback +import logging +import redis + +class StorageHandler: + def __init__(self): + # Configuração de logger + self.logger = logging.getLogger("StorageHandler") + handler = logging.StreamHandler() + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + handler.setFormatter(formatter) + self.logger.addHandler(handler) + self.logger.setLevel(logging.DEBUG) + self.logger.info("StorageHandler inicializado.") + + # Conexão com o Redis + self.redis = redis.Redis( + host=os.getenv('REDIS_HOST', 'localhost'), + port=int(os.getenv('REDIS_PORT', 6380)), + db=0, + decode_responses=True + ) + + # Retenção de logs e backups + self.log_retention_hours = int(os.getenv('LOG_RETENTION_HOURS', 48)) + self.backup_retention_days = int(os.getenv('BACKUP_RETENTION_DAYS', 7)) + + def _get_redis_key(self, key): + return f"transcrevezap:{key}" + + def add_log(self, level: str, message: str, metadata: dict = None): + log_entry = { + "timestamp": datetime.now().isoformat(), + "level": level, + "message": message, + "metadata": json.dumps(metadata) if metadata else None + } + self.redis.lpush(self._get_redis_key("logs"), json.dumps(log_entry)) + self.redis.ltrim(self._get_redis_key("logs"), 0, 999) # Manter apenas os últimos 1000 logs + self.logger.log(getattr(logging, level.upper(), logging.INFO), f"{message} | Metadata: {metadata}") + + def get_allowed_groups(self) -> List[str]: + return self.redis.smembers(self._get_redis_key("allowed_groups")) + + def add_allowed_group(self, group: str): + self.redis.sadd(self._get_redis_key("allowed_groups"), group) + + def remove_allowed_group(self, group: str): + self.redis.srem(self._get_redis_key("allowed_groups"), group) + + def get_blocked_users(self) -> List[str]: + return self.redis.smembers(self._get_redis_key("blocked_users")) + + def add_blocked_user(self, user: str): + self.redis.sadd(self._get_redis_key("blocked_users"), user) + + def remove_blocked_user(self, user: str): + self.redis.srem(self._get_redis_key("blocked_users"), user) + + def get_statistics(self) -> Dict: + total_processed = int(self.redis.get(self._get_redis_key("total_processed")) or 0) + last_processed = self.redis.get(self._get_redis_key("last_processed")) + daily_count = json.loads(self.redis.get(self._get_redis_key("daily_count")) or "{}") + group_count = json.loads(self.redis.get(self._get_redis_key("group_count")) or "{}") + user_count = json.loads(self.redis.get(self._get_redis_key("user_count")) or "{}") + error_count = int(self.redis.get(self._get_redis_key("error_count")) or 0) + success_rate = float(self.redis.get(self._get_redis_key("success_rate")) or 100.0) + + return { + "total_processed": total_processed, + "last_processed": last_processed, + "stats": { + "daily_count": daily_count, + "group_count": group_count, + "user_count": user_count, + "error_count": error_count, + "success_rate": success_rate, + } + } + + def can_process_message(self, remote_jid): + try: + allowed_groups = self.get_allowed_groups() + blocked_users = self.get_blocked_users() + + if remote_jid in blocked_users: + return False + if "@g.us" in remote_jid and remote_jid not in allowed_groups: + return False + + return True + except Exception as e: + self.logger.error(f"Erro ao verificar se pode processar mensagem: {e}") + return False + + def record_processing(self, remote_jid): + try: + # Incrementar total processado + self.redis.incr(self._get_redis_key("total_processed")) + + # Atualizar último processamento + self.redis.set(self._get_redis_key("last_processed"), datetime.now().isoformat()) + + # Atualizar contagem diária + today = datetime.now().strftime("%Y-%m-%d") + daily_count = json.loads(self.redis.get(self._get_redis_key("daily_count")) or "{}") + daily_count[today] = daily_count.get(today, 0) + 1 + self.redis.set(self._get_redis_key("daily_count"), json.dumps(daily_count)) + + # Atualizar contagem de grupo ou usuário + if "@g.us" in remote_jid: + group_count = json.loads(self.redis.get(self._get_redis_key("group_count")) or "{}") + group_count[remote_jid] = group_count.get(remote_jid, 0) + 1 + self.redis.set(self._get_redis_key("group_count"), json.dumps(group_count)) + else: + user_count = json.loads(self.redis.get(self._get_redis_key("user_count")) or "{}") + user_count[remote_jid] = user_count.get(remote_jid, 0) + 1 + self.redis.set(self._get_redis_key("user_count"), json.dumps(user_count)) + + # Atualizar taxa de sucesso + total = int(self.redis.get(self._get_redis_key("total_processed")) or 0) + errors = int(self.redis.get(self._get_redis_key("error_count")) or 0) + success_rate = ((total - errors) / total) * 100 if total > 0 else 100 + self.redis.set(self._get_redis_key("success_rate"), success_rate) + + except Exception as e: + self.logger.error(f"Erro ao registrar processamento: {e}") + + def record_error(self): + self.redis.incr(self._get_redis_key("error_count")) + + def clean_old_logs(self): + try: + cutoff_time = datetime.now() - timedelta(hours=self.log_retention_hours) + logs = self.redis.lrange(self._get_redis_key("logs"), 0, -1) + for log in logs: + log_entry = json.loads(log) + if datetime.fromisoformat(log_entry["timestamp"]) < cutoff_time: + self.redis.lrem(self._get_redis_key("logs"), 0, log) + else: + break # Assumindo que os logs estão ordenados por tempo + except Exception as e: + self.logger.error(f"Erro ao limpar logs antigos: {e}") + + def backup_data(self): + try: + data = { + "allowed_groups": list(self.get_allowed_groups()), + "blocked_users": list(self.get_blocked_users()), + "statistics": self.get_statistics(), + } + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_key = f"backup:{timestamp}" + self.redis.set(backup_key, json.dumps(data)) + self.redis.expire(backup_key, self.backup_retention_days * 24 * 60 * 60) # Expira após os dias de retenção + except Exception as e: + self.logger.error(f"Erro ao criar backup: {e}") + + def clean_old_backups(self): + try: + for key in self.redis.scan_iter("backup:*"): + if self.redis.ttl(key) <= 0: + self.redis.delete(key) + except Exception as e: + self.logger.error(f"Erro ao limpar backups antigos: {e}") \ No newline at end of file diff --git a/transcription_logs.json b/transcription_logs.json new file mode 100644 index 0000000..e69de29