dom2 / app.py
MLMPBR's picture
Update app.py
23b8768 verified
Raw History Blame Contribute Delete
16.7 kB
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
import requests
import xml.etree.ElementTree as ET
import google.generativeai as genai
import resend
from datetime import datetime, timedelta
import os
import time
from bs4 import BeautifulSoup
import pytz
import traceback
app = FastAPI()
# ─── CONFIGURAÇÃO ───────────────────────────────────────────────
RESEND_API_KEY = os.getenv("RESEND_API_KEY")
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
# βœ… DOMÍNIO PRΓ“PRIO CONFIGURADO
EMAIL_REMETENTE = os.getenv("EMAIL_REMETENTE", "falecom@mariomello.com.br")
EMAIL_DESTINO = os.getenv("EMAIL_DESTINO", "marioluiz_mello@hotmail.com")
TZ_BRASILIA = pytz.timezone("America/Sao_Paulo")
# ─── VARIÁVEL GLOBAL DE ESTADO ──────────────────────────────────
ultimo_envio = {"status": "Nunca executado", "data": None, "atos": 0, "log": []}
def log(msg: str):
timestamp = datetime.now(TZ_BRASILIA).strftime("%H:%M:%S")
entry = f"[{timestamp}] {msg}"
ultimo_envio["log"].append(entry)
print(entry)
if len(ultimo_envio["log"]) > 100:
ultimo_envio["log"] = ultimo_envio["log"][-100:]
# ─── VALIDAÇÃO ─────────────────────────────────────────────────
log("=" * 50)
log("πŸš€ AGENTE SEPAF INICIANDO...")
log(f"πŸ“§ Remetente configurado: {EMAIL_REMETENTE}")
log("=" * 50)
if not RESEND_API_KEY:
log("❌ CRÍTICO: RESEND_API_KEY não configurada!")
else:
log("βœ… RESEND_API_KEY OK")
if not GOOGLE_API_KEY:
log("❌ CRÍTICO: GOOGLE_API_KEY não configurada!")
else:
log("βœ… GOOGLE_API_KEY OK")
# ─── LEITURA DO emails.txt ──────────────────────────────────────
lista_emails = []
EMAILS_TXT_PATHS = [
"/mnt/agents/upload/emails.txt",
"./emails.txt",
"/app/emails.txt",
"emails.txt",
]
for path in EMAILS_TXT_PATHS:
if os.path.exists(path):
try:
with open(path, "r", encoding="utf-8") as f:
linhas = f.readlines()
for linha in linhas:
email = linha.strip()
if email and "@" in email:
lista_emails.append(email)
log(f"πŸ“„ emails.txt encontrado em: {path}")
log(f" {len(lista_emails)} e-mails: {lista_emails}")
break
except Exception as e:
log(f"⚠️ Erro ao ler {path}: {e}")
if not lista_emails:
lista_emails = [EMAIL_DESTINO]
log(f"πŸ“§ Usando e-mail padrΓ£o: {EMAIL_DESTINO}")
resend.api_key = RESEND_API_KEY
# ─── FUNÇÃO DE TESTE DO RESEND ─────────────────────────────────
def testar_resend():
if not RESEND_API_KEY:
return "❌ RESEND_API_KEY não configurada"
try:
log("πŸ§ͺ TESTANDO RESEND...")
params = {
"from": EMAIL_REMETENTE,
"to": lista_emails[0],
"subject": "πŸ§ͺ TESTE SEPAF - VerificaΓ§Γ£o de DomΓ­nio PrΓ³prio",
"html": f"""
<h2>βœ… Teste de conectividade SEPAF</h2>
<p>Se vocΓͺ recebeu este e-mail, o domΓ­nio <b>{EMAIL_REMETENTE}</b> estΓ‘ verificado e funcionando corretamente no Resend.</p>
<p>HorΓ‘rio do teste: {datetime.now(TZ_BRASILIA).strftime('%d/%m/%Y %H:%M:%S')}</p>
""",
}
resultado = resend.Emails.send(params)
headers = resultado.get("http_headers", {})
daily = headers.get("x-resend-daily-quota", "?")
monthly = headers.get("x-resend-monthly-quota", "?")
log(f"βœ… TESTE: ID={resultado.get('id')}")
log(f" πŸ“Š Cota: {daily}/dia | {monthly}/mΓͺs")
return f"Teste enviado! ID: {resultado.get('id')} | Cota: {daily}/dia, {monthly}/mΓͺs"
except Exception as e:
log(f"❌ TESTE FALHOU: {e}")
log(traceback.format_exc())
return f"❌ Falha: {e}"
# ─── FUNÇÕES AUXILIARES ─────────────────────────────────────────
def extrair_texto_pagina(url: str) -> str:
try:
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
resposta = requests.get(url, headers=headers, timeout=15)
resposta.raise_for_status()
soup = BeautifulSoup(resposta.text, "html.parser")
for tag in soup(["script", "style", "nav", "footer", "header"]):
tag.decompose()
texto = soup.get_text(separator=" ", strip=True)
return texto[:4000]
except Exception as e:
log(f"❌ Erro ao raspar {url}: {e}")
return ""
def executar_agente_diario():
global ultimo_envio
agora_brasilia = datetime.now(TZ_BRASILIA)
log("=" * 50)
log(f"πŸš€ INICIANDO AGENTE em {agora_brasilia}")
log("=" * 50)
hoje = agora_brasilia
data_fim = hoje.strftime("%d/%m/%Y")
data_inicio = (hoje - timedelta(days=7)).strftime("%d/%m/%Y")
url_rss = (
f"https://diariomunicipal.sc.gov.br/?r=site/portal"
f"&codigoEntidade=173&dataInicial={data_inicio}"
f"&dataFinal={data_fim}&view=rss"
)
log(f"πŸ”— RSS: {url_rss}")
try:
headers = {"User-Agent": "Mozilla/5.0"}
resposta_rss = requests.get(url_rss, headers=headers, timeout=20)
resposta_rss.raise_for_status()
root = ET.fromstring(resposta_rss.content)
itens = root.findall(".//item")
log(f"πŸ“° {len(itens)} itens no RSS")
except Exception as e:
msg = f"❌ ERRO RSS/XML: {e}"
log(msg)
ultimo_envio = {**ultimo_envio, "status": msg, "data": agora_brasilia.isoformat(), "atos": 0}
return msg
if not itens:
msg = f"⚠️ VAZIO: Nenhuma publicação entre {data_inicio} e {data_fim}."
log(msg)
ultimo_envio = {**ultimo_envio, "status": msg, "data": agora_brasilia.isoformat(), "atos": 0}
return msg
# Raspagem
lista_final = []
for idx, item in enumerate(itens):
titulo_elem = item.find("title")
link_elem = item.find("link")
titulo = titulo_elem.text.strip() if titulo_elem is not None and titulo_elem.text else "TΓ­tulo nΓ£o disponΓ­vel"
link_ato = link_elem.text.strip() if link_elem is not None and link_elem.text else ""
log(f" [{idx+1}/{len(itens)}] {titulo[:60]}...")
texto_ato = extrair_texto_pagina(link_ato) if link_ato else ""
lista_final.append({"titulo": titulo, "link": link_ato, "texto": texto_ato})
time.sleep(0.5)
log(f"βœ… {len(lista_final)} atos processados")
if not lista_final:
msg = "⚠️ Nenhum ato vÑlido."
ultimo_envio = {**ultimo_envio, "status": msg, "data": agora_brasilia.isoformat(), "atos": 0}
return msg
# Gemini
if not GOOGLE_API_KEY:
msg = "❌ GOOGLE_API_KEY não configurada."
log(msg)
ultimo_envio = {**ultimo_envio, "status": msg, "data": agora_brasilia.isoformat(), "atos": 0}
return msg
log("πŸ€– Enviando para Gemini...")
genai.configure(api_key=GOOGLE_API_KEY)
model = genai.GenerativeModel("gemini-2.5-flash")
bloco_dados = ""
for i, a in enumerate(lista_final):
bloco_dados += f"--- ATO {i+1} ---\nTÍTULO: {a['titulo']}\nLINK: {a['link']}\nTEXTO: {a['texto']}\n\n"
prompt = f"""
VocΓͺ Γ© um Auditor AnalΓ­tico da SEPAF Navegantes. Recebemos {len(lista_final)} atos oficiais.
Crie um relatΓ³rio HTML profissional. Para cada ato:
- TΓ­tulo Original
- Resumo executivo (Nomes, CNPJs, Valores R$, decisΓ£o)
- πŸ”— <a href="LINK">CLIQUE AQUI PARA VER MATΓ‰RIA COMPLETA</a>
Use CSS inline. Devolva APENAS HTML puro, sem Markdown.
DADOS:
{bloco_dados}
"""
try:
response = model.generate_content(prompt)
resumo_html = response.text.replace("```html", "").replace("```", "").strip()
log("βœ… Gemini OK")
except Exception as e:
msg = f"❌ ERRO GEMINI: {e}"
log(msg)
log(traceback.format_exc())
ultimo_envio = {**ultimo_envio, "status": msg, "data": agora_brasilia.isoformat(), "atos": 0}
return msg
# Resend
if not RESEND_API_KEY:
msg = "❌ RESEND_API_KEY não configurada."
log(msg)
ultimo_envio = {**ultimo_envio, "status": msg, "data": agora_brasilia.isoformat(), "atos": 0}
return msg
log(f"πŸ“§ Enviando para {len(lista_emails)} destinatΓ‘rios...")
emails_enviados = 0
emails_falhos = 0
cota_info = "N/A"
for email_destino in lista_emails:
try:
log(f" πŸ“€ Enviando para {email_destino}...")
params = {
"from": EMAIL_REMETENTE,
"to": email_destino,
"subject": f"πŸ›οΈ SEPAF: DiΓ‘rio Navegantes ({data_inicio} a {data_fim})",
"html": resumo_html,
}
resultado = resend.Emails.send(params)
headers = resultado.get("http_headers", {})
daily = headers.get("x-resend-daily-quota", "?")
monthly = headers.get("x-resend-monthly-quota", "?")
cota_info = f"{daily}/dia, {monthly}/mΓͺs"
log(f" βœ… ID: {resultado.get('id')} | Cota: {cota_info}")
emails_enviados += 1
if daily != "?" and int(daily) <= 0:
log(" 🚨 COTA DIÁRIA ESGOTADA!")
break
except Exception as e:
log(f" ❌ Falha para {email_destino}: {e}")
emails_falhos += 1
if emails_enviados > 0:
msg = f"βœ… {emails_enviados}/{len(lista_emails)} enviados. Cota: {cota_info}"
else:
msg = f"❌ Nenhum e-mail enviado. {emails_falhos} falhas. Cota: {cota_info}"
log(msg)
ultimo_envio = {
**ultimo_envio,
"status": msg,
"data": agora_brasilia.isoformat(),
"atos": len(lista_final),
"periodo": f"{data_inicio} a {data_fim}",
"emails_enviados": emails_enviados,
"emails_falhos": emails_falhos,
"cota": cota_info
}
return msg
# ─── AGENDADOR ──────────────────────────────────────────────────
scheduler = BackgroundScheduler(timezone=TZ_BRASILIA)
scheduler.add_job(
executar_agente_diario,
trigger=CronTrigger(hour=11, minute=30),
id="agente_diario",
name="Agente DiΓ‘rio Oficial",
replace_existing=True,
)
scheduler.start()
log("⏰ Agendador: 11:30 BRT")
# ─── ROTAS ──────────────────────────────────────────────────────
@app.get("/", response_class=HTMLResponse)
def home():
status = ultimo_envio.get("status", "Nunca executado")
data = ultimo_envio.get("data", "β€”")
atos = ultimo_envio.get("atos", 0)
emails_env = ultimo_envio.get("emails_enviados", 0)
emails_fal = ultimo_envio.get("emails_falhos", 0)
cota = ultimo_envio.get("cota", "N/A")
log_html = "<br>".join(ultimo_envio.get("log", [])[-25:])
return f"""
<html>
<head><meta charset="UTF-8"><title>SEPAF - Agente DiΓ‘rio</title>
<style>
body {{ font-family: Arial, sans-serif; background: #f4f7f6; padding: 30px; }}
.container {{ max-width: 950px; margin: auto; }}
.card {{ background: white; padding: 25px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); margin-bottom: 20px; }}
h2 {{ color: #004a80; margin-top: 0; }}
.status-box {{ background: #e8f4f8; padding: 15px; border-radius: 5px; font-size: 14px; line-height: 1.6; }}
.alert {{ background: #d4edda; border: 1px solid #28a745; padding: 12px; border-radius: 5px; margin: 10px 0; }}
.alert-warning {{ background: #fff3cd; border-color: #ffc107; }}
.log-box {{ background: #1e1e1e; color: #00ff00; padding: 15px; border-radius: 5px; font-family: monospace; font-size: 12px; max-height: 350px; overflow-y: auto; line-height: 1.4; }}
.btn {{ display: inline-block; padding: 12px 25px; color: white; text-decoration: none; border-radius: 5px; font-weight: bold; margin: 5px; }}
.btn-test {{ background: #ff6b35; }}
.btn-manual {{ background: #28a745; }}
</style></head>
<body>
<div class="container">
<div class="card">
<h2>πŸ›οΈ Painel SEPAF - Agente DiΓ‘rio Oficial</h2>
<p style="color: #555;">RSS Público + Gemini 2.5 Flash + Resend | ⏰ 10:00 BRT</p>
<div class="alert">
<strong>βœ… DOMÍNIO PRΓ“PRIO CONFIGURADO:</strong> {EMAIL_REMETENTE}<br>
Cota atual: <b>{cota}</b> | DestinatΓ‘rios: <b>{len(lista_emails)}</b>
</div>
<div class="status-box">
<strong>πŸ“Š Última ExecuΓ§Γ£o:</strong><br>
Status: <b>{status}</b><br>
Data: {data}<br>
Atos: {atos} | Enviados: {emails_env} | Falhas: {emails_fal}<br>
DestinatΓ‘rios: {", ".join(lista_emails) if lista_emails else "Nenhum"}
</div>
<br>
<a href="/testar-resend" class="btn btn-test">πŸ§ͺ Testar Resend</a>
<a href="/disparar-robo-secreto-sepaf" class="btn btn-manual">▢️ Executar Agente</a>
</div>
<div class="card">
<h3>πŸ“‹ Log de OperaΓ§Γ΅es</h3>
<div class="log-box">{log_html}</div>
</div>
</div>
</body>
</html>
"""
@app.get("/testar-resend", response_class=HTMLResponse)
def testar_resend_rota():
resultado = testar_resend()
log_html = "<br>".join(ultimo_envio.get("log", [])[-20:])
return f"""
<html>
<body style="font-family: Arial; padding: 50px; background: #f4f7f6;">
<div style="max-width: 700px; margin: auto; background: white; padding: 40px; border-radius: 10px;">
<h2 style="color: #ff6b35;">πŸ§ͺ Resultado do Teste Resend</h2>
<div style="padding: 20px; border: 2px solid #ff6b35; border-radius: 5px; font-size: 16px; margin: 20px 0;">
<b>{resultado}</b>
</div>
<p>πŸ“§ Verifique a caixa de entrada (e SPAM) de: <b>{lista_emails[0] if lista_emails else 'Nenhum'}</b></p>
<div style="background: #d4edda; padding: 15px; border-radius: 5px; margin: 20px 0;">
<strong>βœ… DomΓ­nio configurado:</strong> {EMAIL_REMETENTE}<br>
Se nΓ£o recebeu, verifique se o domΓ­nio estΓ‘ verificado no Resend.
</div>
<a href="/" style="color: #004a80; font-weight: bold;">⬅️ Voltar</a>
</div>
<div style="max-width: 700px; margin: 20px auto; background: #1e1e1e; color: #00ff00; padding: 15px; border-radius: 5px; font-family: monospace; font-size: 12px;">
<h4 style="color: white;">Log:</h4>{log_html}
</div>
</body>
</html>
"""
@app.get("/disparar-robo-secreto-sepaf", response_class=HTMLResponse)
def disparar_robo():
resultado = executar_agente_diario()
log_html = "<br>".join(ultimo_envio.get("log", [])[-25:])
return f"""
<html>
<body style="font-family: Arial; padding: 50px; background: #f4f7f6;">
<div style="max-width: 700px; margin: auto; background: white; padding: 40px; border-radius: 10px;">
<h2 style="color: #004a80;">πŸ”Ž Resultado da ExecuΓ§Γ£o</h2>
<div style="padding: 20px; border: 1px solid #ccc; border-radius: 5px; font-size: 16px; white-space: pre-wrap;">
{resultado}
</div>
<br><a href="/" style="color: #004a80; font-weight: bold;">⬅️ Voltar</a>
</div>
<div style="max-width: 700px; margin: 20px auto; background: #1e1e1e; color: #00ff00; padding: 15px; border-radius: 5px; font-family: monospace; font-size: 12px;">
<h4 style="color: white;">Log:</h4>{log_html}
</div>
</body>
</html>
"""
@app.get("/status")
def status():
return ultimo_envio
@app.on_event("shutdown")
def shutdown_scheduler():
scheduler.shutdown()