Download app.py from pmnvt/dom2: direct link, hf CLI and curl.
- Browser
- Download file 16.7 kB
-
https://huggingface.co/spaces/pmnvt/dom2/resolve/main/app.py
- Command line
-
hf download hf://spaces/pmnvt/dom2/app.py
-
curl -L -o app.py https://huggingface.co/spaces/pmnvt/dom2/resolve/main/app.py
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 ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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> | |
| """ | |
| 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> | |
| """ | |
| 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> | |
| """ | |
| def status(): | |
| return ultimo_envio | |
| def shutdown_scheduler(): | |
| scheduler.shutdown() |