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 98af480..12905cd 100644
Binary files a/fluxo.png and b/fluxo.png differ
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 0000000..12905cd
Binary files /dev/null and b/static/fluxo.png differ
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