Skip to content

Instantly share code, notes, and snippets.

@Jeiel0rbit
Created August 15, 2025 18:44
Show Gist options
  • Select an option

  • Save Jeiel0rbit/63039b161e5f8cd1ac3d28d495097b2d to your computer and use it in GitHub Desktop.

Select an option

Save Jeiel0rbit/63039b161e5f8cd1ac3d28d495097b2d to your computer and use it in GitHub Desktop.
# -*- coding: utf-8 -*-
"""
Este script busca as notícias mais relevantes da semana no TabNews,
enriquece os dados buscando o conteúdo completo de cada artigo de forma concorrente,
e utiliza uma IA para formatar um resumo para ser postado no Discord.
"""
import subprocess
import json
import sys
import time
import itertools
import threading
from datetime import datetime, timezone, timedelta
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List, Dict, Optional, Any
# --- CONFIGURAÇÕES GLOBAIS ---
# API Endpoints
TABNEWS_BASE_URL = "https://www.tabnews.com.br/api/v1"
OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions"
# Credenciais e Modelos
OPENROUTER_API_KEY = "sk-or-v1-000000..."
OPENROUTER_MODEL = "deepseek/deepseek-r1:free"
# Parâmetros de Comportamento
USER_AGENT = "Square News Project (https://github.com/user/repo)" # Use um User-Agent descritivo
DAYS_TO_FETCH = 7
ARTICLE_LIMIT = 7
REQUEST_TIMEOUT = 15 # Segundos
# --- PROMPT PARA A IA (OTIMIZADO) ---
PROMPT_TEMPLATE = """
Você é um editor de notícias para Discord. Sua tarefa é analisar os 7 artigos (título, URL e corpo) fornecidos abaixo e criar uma newsletter.
Siga este formato de saída EXATAMENTE, substituindo os placeholders:
---
:newspaper: **11h — [Square News](https://is.gd/Square_News)**
**TÍTULO_DO_ARTIGO_1** — DESCRIÇÃO_CONCISA_1
> [CATEGORIA_1](<URL_DA_FONTE_1>)
**TÍTULO_DO_ARTIGO_2** — DESCRIÇÃO_CONCISA_2
> [CATEGORIA_2](<URL_DA_FONTE_2>)
... e assim por diante para os 7 artigos.
REGRAS IMPORTANTES:
1. **Descrição**: Deve ser ultra-concisa (máximo 200 caracteres) e baseada no corpo do artigo.
2. **Título**: Use o título original do artigo, sem adicionar colchetes ou aspas.
3. **Link na Categoria**: Se um artigo não tiver URL_FONTE, formate a categoria sem o link, assim: `> CATEGORIA`.
Artigos para formatar:
{articles_content}
"""
# --- CLASSE DE FEEDBACK VISUAL ---
class Spinner:
"""Gerencia uma animação de spinner no terminal de forma segura."""
def __init__(self, message: str = "Processando...", delay: float = 0.1):
self._spinner = itertools.cycle(['-', '/', '|', '\\'])
self._delay = delay
self._busy = False
self._spinner_visible = False
self.message = message
self._thread = None
def _spinner_task(self):
while self._busy:
char = next(self._spinner)
sys.stdout.write(f'\r{self.message} {char}')
sys.stdout.flush()
self._spinner_visible = True
time.sleep(self._delay)
def start(self):
"""Inicia a animação do spinner."""
self._busy = True
self._thread = threading.Thread(target=self._spinner_task)
self._thread.start()
def stop(self):
"""Para a animação e limpa a linha."""
self._busy = False
if self._thread:
self._thread.join()
if self._spinner_visible:
sys.stdout.write(f'\r{self.message} ✓ \n')
sys.stdout.flush()
self._spinner_visible = False
# --- FUNÇÕES DE COMUNICAÇÃO COM APIS ---
def run_curl_command(url: str) -> Optional[Any]:
"""Executa um comando cURL e retorna o JSON decodificado."""
try:
command = ['curl', '--silent', '--location', '--header', f'User-Agent: {USER_AGENT}', url]
result = subprocess.run(
command,
capture_output=True,
text=True,
check=True,
encoding='utf-8',
timeout=REQUEST_TIMEOUT
)
return json.loads(result.stdout)
except FileNotFoundError:
print("\n[ERRO] Comando 'curl' não encontrado. Verifique se ele está instalado e no PATH.", file=sys.stderr)
return None
except subprocess.TimeoutExpired:
print(f"\n[AVISO] A requisição para {url} demorou demais (timeout).", file=sys.stderr)
return None
except subprocess.CalledProcessError:
print(f"\n[AVISO] Falha ao buscar dados de {url}. O servidor pode estar indisponível.", file=sys.stderr)
return None
except json.JSONDecodeError:
print(f"\n[AVISO] Não foi possível decodificar a resposta JSON de {url}.", file=sys.stderr)
return None
def fetch_main_article_list() -> List[Dict]:
"""Busca a lista principal de artigos relevantes do TabNews."""
url = f"{TABNEWS_BASE_URL}/contents/NewsletterOficial?strategy=relevant"
data = run_curl_command(url)
if isinstance(data, list):
return data
return []
def fetch_article_details(article_meta: Dict) -> Optional[Dict]:
"""Busca o corpo e outros detalhes de um único artigo."""
owner = article_meta.get("owner_username")
slug = article_meta.get("slug")
if not owner or not slug:
return None
url = f"{TABNEWS_BASE_URL}/contents/{owner}/{slug}"
details = run_curl_command(url)
if details:
# Combina os metadados originais com os detalhes buscados
return {**article_meta, "body": details.get("body", "")}
return None
# --- LÓGICA DE PROCESSAMENTO ---
def filter_and_select_articles(articles: List[Dict]) -> List[Dict]:
"""Filtra artigos da última semana e seleciona os melhores por tabcoins."""
if not articles:
print("[INFO] Nenhuma lista de artigos recebida para processar.")
return []
print(f"[INFO] {len(articles)} artigos recebidos da API para análise.")
now = datetime.now(timezone.utc)
time_threshold = now - timedelta(days=DAYS_TO_FETCH)
try:
weekly_articles = [
article for article in articles
if "published_at" in article and datetime.fromisoformat(article["published_at"].replace('Z', '+00:00')) >= time_threshold
]
except (TypeError, ValueError) as e:
print(f"\n[ERRO] Formato de data inesperado nos artigos: {e}", file=sys.stderr)
return []
print(f"[INFO] {len(weekly_articles)} artigos encontrados nos últimos {DAYS_TO_FETCH} dias.")
weekly_articles.sort(key=lambda x: x.get('tabcoins', 0), reverse=True)
return weekly_articles[:ARTICLE_LIMIT]
def enrich_articles_with_content(articles: List[Dict]) -> List[Dict]:
"""Busca o conteúdo completo de cada artigo de forma concorrente."""
enriched_articles = []
with ThreadPoolExecutor(max_workers=ARTICLE_LIMIT) as executor:
future_to_article = {executor.submit(fetch_article_details, article): article for article in articles}
for i, future in enumerate(as_completed(future_to_article), 1):
sys.stdout.write(f"\rBuscando conteúdo dos artigos... [{i}/{ARTICLE_LIMIT}]")
sys.stdout.flush()
result = future.result()
if result and result.get("body"):
enriched_articles.append(result)
print(" ✓") # Completa a linha de progresso
return enriched_articles
def format_newsletter_with_ai(articles: List[Dict]) -> Optional[str]:
"""Monta o prompt e envia para a IA para formatação final."""
content_blocks = []
for i, article in enumerate(articles, 1):
title = article.get("title", "Sem Título")
source_url = article.get("source_url", "")
body = article.get("body", "Sem conteúdo.")
content_blocks.append(f"--- ARTIGO {i} ---\nTÍTULO: {title}\nURL_FONTE: {source_url}\nCORPO: {body}\n")
prompt_content = PROMPT_TEMPLATE.format(articles_content="\n".join(content_blocks))
payload = {
"model": OPENROUTER_MODEL,
"messages": [{"role": "user", "content": prompt_content}],
"max_tokens": 2048,
"temperature": 0.5,
}
try:
command = [
'curl', '--silent', '--request', 'POST',
'--url', OPENROUTER_API_URL,
'--header', f'Authorization: Bearer {OPENROUTER_API_KEY}',
'--header', 'Content-Type: application/json',
'--data', json.dumps(payload)
]
result = subprocess.run(command, capture_output=True, text=True, check=True, encoding='utf-8', timeout=60)
response_data = json.loads(result.stdout)
content = response_data.get("choices", [{}])[0].get("message", {}).get("content")
if content and content.strip():
return content
else:
print("[ERRO] A resposta da IA está vazia ou em formato inesperado.", file=sys.stderr)
print(json.dumps(response_data, indent=2))
return None
except Exception as e:
print(f"\n[ERRO] Falha ao comunicar com a API da OpenRouter: {e}", file=sys.stderr)
return None
# --- FUNÇÃO PRINCIPAL ---
def main():
"""Orquestra a execução completa do script."""
spinner = Spinner("Buscando lista de notícias no TabNews...")
spinner.start()
initial_articles = fetch_main_article_list()
spinner.stop()
if not initial_articles:
print("[FALHA] Não foi possível obter a lista inicial de artigos. Encerrando.")
return
top_articles_meta = filter_and_select_articles(initial_articles)
if not top_articles_meta:
print("[INFO] Nenhum artigo relevante encontrado na última semana. Encerrando.")
return
enriched_articles = enrich_articles_with_content(top_articles_meta)
if not enriched_articles:
print("[FALHA] Não foi possível buscar o conteúdo de nenhum artigo. Encerrando.")
return
print(f"[INFO] Conteúdo de {len(enriched_articles)}/{len(top_articles_meta)} artigos obtido com sucesso.")
spinner = Spinner(f"Formatando notícias com {OPENROUTER_MODEL}...")
spinner.start()
formatted_news = format_newsletter_with_ai(enriched_articles)
spinner.stop()
if formatted_news:
print("\n--- Notícias Formatadas para o Discord ---\n")
print(formatted_news)
else:
print("\n[FALHA] Não foi possível formatar as notícias.")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment