Created
August 15, 2025 18:44
-
-
Save Jeiel0rbit/63039b161e5f8cd1ac3d28d495097b2d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # -*- 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