@danhachuel/thunderbolt 0.3.34 → 0.3.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Este manual descreve a instalação local da UI Thunderbolt, baseada no MoneyPrinterTurbo, utilizando o pacote npm `@danhachuel/thunderbolt`. O fluxo recomendado instala automaticamente o ambiente Python, as dependências da aplicação, as dependências do MoneyPrinterTurbo, o Streamlit e o suporte FFmpeg através de `imageio-ffmpeg`.
4
4
 
5
- > **Versão deste manual:** 0.2.89
5
+ > **Versão deste manual:** 0.3.35
6
6
  > **Pacote npm:** `@danhachuel/thunderbolt`
7
7
  > **Porta padrão da UI:** `localhost:3030`
8
8
  > **Repositório:** [github.com/DanHachuel/thunderbolt](https://github.com/DanHachuel/thunderbolt)
@@ -100,13 +100,13 @@ Execute:
100
100
  Windows PowerShell ou MobaXterm:
101
101
 
102
102
  ```powershell
103
- npx.cmd --yes @danhachuel/thunderbolt@0.2.77 install
103
+ npx.cmd --yes @danhachuel/thunderbolt@0.3.35 install
104
104
  ```
105
105
 
106
106
  Linux/macOS:
107
107
 
108
108
  ```bash
109
- npx --yes @danhachuel/thunderbolt@0.2.77 install
109
+ npx --yes @danhachuel/thunderbolt@0.3.35 install
110
110
  ```
111
111
 
112
112
  A instalação normal é **segura para actualizações**: preserva `storage`, Blueprints, Brandings, configurações e artefactos do utilizador. Remove apenas `.venv`, o clone técnico do MoneyPrinterTurbo e dependências que serão recriadas. Uma pasta antiga sem dados do utilizador, como `C:\Users\<utilizador>\AppData\Local\hermes` da tentativa incompleta, pode ser removida; uma pasta antiga que contenha Blueprints, Brandings ou storage é preservada e apenas avisada no terminal. Feche processos Python, Node, Streamlit e MobaXterm que estejam a usar as pastas antes de executar.
@@ -196,7 +196,7 @@ O Thunderbolt inclui as dez opções de idioma usadas pelo fluxo MoneyPrinterTur
196
196
 
197
197
  No topo da área principal, o menu nativo de idioma segue o padrão do MoneyPrinterTurbo e mostra o rótulo **Language** acima do selector. Cada opção apresenta o nome e o código, acompanhados por uma imagem SVG local da bandeira; não depende de emojis nem de siglas da fonte do sistema. A escolha fica guardada como `ui_language` em `storage/state/settings.json` e, quando existe um caminho MoneyPrinterTurbo configurado, é sincronizada com `[ui].language` no `config.toml`. O dashboard inicial, o interior das páginas e as abas da barra lateral usam o mesmo idioma seleccionado. O toolbar do Streamlit não é manipulado: o indicador de execução, **Deploy** e o menu de três pontos permanecem nativos e clicáveis. Na Criação de Vídeos e em Roteiros, o selector **Script Language** mantém o catálogo de vídeo independente, mas guarda o código curto; essa escolha é sincronizada com `[ui].video_language`.
198
198
 
199
- Todas as subabas internas e o conteúdo das páginas também são traduzidos nos dez idiomas. Isso inclui títulos, subtítulos, descrições, labels, placeholders, opções, botões, avisos, mensagens de sucesso/erro, estados vazios, métricas, expanderes e blocos Markdown/HTML. Valores técnicos, IDs, URLs, nomes de ficheiros e dados introduzidos pelo utilizador permanecem inalterados. As subabas internas traduzidas incluem: Blueprints/Brandings, Pesquisa pública/Cadastro manual/Contas cadastradas, Upload/Biblioteca, Importar do YouTube/Canais em lote gmail/Cadastro manual, Criar vídeo/Vídeos, Novo roteiro/letra/Histórico guardado, análise de clusters, edição de cortes, Vídeos/Código Python, as quatro opções de Upload, API Keys/Teste de vozes, Serviços e modelos/Fontes de materiais e Client MCP/Servidor MCP/Skill. A ordem e as chaves internas dos widgets permanecem estáveis.
199
+ Todas as subabas internas e o conteúdo das páginas também são traduzidos nos dez idiomas. Isso inclui títulos, subtítulos, descrições, labels, placeholders, opções, botões, avisos, mensagens de sucesso/erro, estados vazios, métricas, expanderes e blocos Markdown/HTML. Valores técnicos, IDs, URLs, nomes de ficheiros e dados introduzidos pelo utilizador permanecem inalterados. As subabas internas traduzidas incluem: Blueprints/Brandings, Pesquisa pública/Cadastro manual/Contas cadastradas, Upload/Biblioteca, Importar do YouTube/Canais em lote gmail/Cadastro manual, Criar vídeo/Vídeos, Novo roteiro/letra/Histórico guardado, análise de clusters, edição de cortes, Vídeos/Código Python, as quatro opções de Upload, API Keys/Teste de vozes, Serviços e modelos/Fontes de materiais, Client MCP/Servidor MCP/Skill e Notificações Geral/Telegram. A ordem e as chaves internas dos widgets permanecem estáveis.
200
200
 
201
201
  ### Temas Light e Dark
202
202
 
@@ -357,7 +357,7 @@ Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
357
357
 
358
358
  ## 8. Configuração inicial da UI
359
359
 
360
- Na primeira execução, a barra lateral apresenta **Início**, **Niche Finder**, **Pipeline**, **Pipeline TikTok**, **Automação**, **Edição**, **AI Influencers** e **Configurações**. O menu expansível **Automação** contém a subaba **Automação Youtube**. O menu expansível **Edição** contém **Limpador de Metadados**, **Cortes** e **Editor Python**. O menu expansível **AI Influencers** contém **Personagens**, **Redes Sociais**, **Tutorial Meta** e **Tutorial Supabase**, nessa ordem. O **Tutorial Meta** apresenta o guia de configuração de Instagram e credenciais Meta para automações com n8n. O **Tutorial Supabase** apresenta o guia para criar as tabelas `plans` e `posts` e o bucket `instagram-images`, incluindo a ligação para a fonte original no GitHub; as duas frases promocionais da comunidade foram omitidas. O menu expansível **Niche Finder** contém **Niche Finder Kaggle** e **Niche Finder Apify**. A aba **Niche Finder Kaggle** mantém a análise do dataset Kaggle; **Niche Finder Apify** é uma segunda alternativa independente, com actor, dataset, parâmetros, credencial, execução e resultados próprios. O menu **Pipeline** contém **Criação de Vídeos**, **Criação de Músicas**, **Roteiros** e **Upload**. O menu **Configurações** contém **Canais Youtube**, **Blueprints Youtube**, **MCP**, **Contas Google**, **Configuração API** e **Notificações**. Abra **Configurações > Contas Google** para gerir contas Google/YouTube, cartões, documentos de Upload directo, `sessionInfo` por conta, a `INNERTUBE_API_KEY` global, contas adicionais e a configuração global do YouTube. Abra **Configurações > Configuração API** para configurar as restantes API Keys, providers, modelos, serviços e o **Teste de vozes**. A página **Configurações > Notificações** está reservada para futuros eventos locais. Para YouTube, preencha primeiro o par **OAuth Client ID + OAuth Client Secret** em **Contas Google** se pretende autorizar uploads. A **YouTube Data API Key** é uma credencial Google Cloud diferente e fica em **Contas Google**, apenas para o método oficial de métricas; Client ID + Client Secret não formam uma API Key.
360
+ Na primeira execução, a barra lateral apresenta **Início**, **Niche Finder**, **Pipeline**, **Pipeline TikTok**, **Automação**, **Edição**, **AI Influencers** e **Configurações**. O menu expansível **Automação** contém a subaba **Automação Youtube**. O menu expansível **Edição** contém **Limpador de Metadados**, **Cortes** e **Editor Python**. O menu expansível **AI Influencers** contém **Personagens**, **Redes Sociais**, **Tutorial Meta** e **Tutorial Supabase**, nessa ordem. O **Tutorial Meta** apresenta o guia de configuração de Instagram e credenciais Meta para automações com n8n. O **Tutorial Supabase** apresenta o guia para criar as tabelas `plans` e `posts` e o bucket `instagram-images`, incluindo a ligação para a fonte original no GitHub; as duas frases promocionais da comunidade foram omitidas. O menu expansível **Niche Finder** contém **Niche Finder Kaggle** e **Niche Finder Apify**. A aba **Niche Finder Kaggle** mantém a análise do dataset Kaggle; **Niche Finder Apify** é uma segunda alternativa independente, com actor, dataset, parâmetros, credencial, execução e resultados próprios. O menu **Pipeline** contém **Criação de Vídeos**, **Criação de Músicas**, **Roteiros** e **Upload**. O menu **Configurações** contém **Canais Youtube**, **Blueprints Youtube**, **MCP**, **Contas Google**, **Configuração API** e **Notificações**. Abra **Configurações > Contas Google** para gerir contas Google/YouTube, cartões, documentos de Upload directo, `sessionInfo` por conta, a `INNERTUBE_API_KEY` global, contas adicionais e a configuração global do YouTube. Abra **Configurações > Configuração API** para configurar as restantes API Keys, providers, modelos, serviços e o **Teste de vozes**. A página **Configurações > Notificações** contém as subabas **Geral** e **Telegram**. A subaba Geral mantém o histórico e as preferências dos eventos locais; a subaba Telegram envia esses mesmos eventos para o Chat ID configurado. Para YouTube, preencha primeiro o par **OAuth Client ID + OAuth Client Secret** em **Contas Google** se pretende autorizar uploads. A **YouTube Data API Key** é uma credencial Google Cloud diferente e fica em **Contas Google**, apenas para o método oficial de métricas; Client ID + Client Secret não formam uma API Key.
361
361
 
362
362
  | Configuração | Finalidade |
363
363
  |---|---|
@@ -378,6 +378,16 @@ Na primeira execução, a barra lateral apresenta **Início**, **Niche Finder**,
378
378
  | Apify API Token | Token pessoal da Apify, guardado mascarado no storage local e usado apenas pela alternativa Apify |
379
379
  | Apify Actor ID | Actor usado pela alternativa Apify, por padrão `streamers~youtube-scraper` |
380
380
  | Apify polling/timeout | Intervalo de consulta e limite máximo da execução do actor |
381
+ | Telegram Bot Token | Token criado pelo BotFather, guardado apenas no storage local |
382
+ | Telegram Chat ID | Utilizador, grupo ou canal que receberá as notificações |
383
+ | Telegram Proxy | Proxy HTTP/HTTPS/SOCKS opcional para ambientes sem acesso directo |
384
+ | Telegram timeout | Limite de espera de cada envio, entre 5 e 120 segundos |
385
+
386
+ ### Telegram Gateway
387
+
388
+ Em **Configurações > Notificações > Telegram**, active **Activar notificações Telegram**, introduza o **Telegram Bot Token** criado pelo BotFather e o **Telegram Chat ID** de destino. O botão **Testar chamada API** executa apenas o método read-only `getMe` para validar o bot; não envia uma mensagem de teste. O proxy é opcional e o suporte SOCKS depende das dependências instaladas pelo `requirements.txt`.
389
+
390
+ Depois de guardar, cada nova notificação permitida pelas preferências da subaba **Geral** é enviada pela Bot API oficial do Telegram através de `sendMessage`. A notificação continua a ser persistida localmente mesmo que o Telegram esteja indisponível, o Chat ID esteja errado ou a API devolva um erro; a falha externa nunca interrompe a produção, os uploads ou a UI. A integração é apenas outbound: não recebe comandos, não faz polling e não expõe o Bot Token em logs, metadados ou no GitHub.
381
391
 
382
392
  As credenciais devem ser inseridas apenas na configuração local. A Kaggle API Key pertence exclusivamente à alternativa Kaggle. O Apify API Token pertence exclusivamente à alternativa Apify e não é usado para Kaggle; os resultados e histórico Apify ficam separados em `niche_apify_runs.json`. A Data API Key, o OAuth Client ID e o OAuth Client Secret são valores diferentes; Client ID + Secret não geram uma API Key nem um token OAuth até a conta ser autorizada. Não coloque nenhum deles no GitHub, no `package.json`, em blueprints ou em ficheiros de estado versionados.
383
393
 
@@ -448,7 +458,7 @@ Ao abrir a página, o Thunderbolt não prepara dados públicos, não descarrega
448
458
 
449
459
  Os parâmetros da UI são número de clusters entre 2 e 10, suporte mínimo entre 0,01 e 0,50, país, engagement, intervalo de datas e tags, todos dentro da área principal da aba. O núcleo normaliza os dados, calcula engagement, aplica filtros, faz transformação logarítmica e standardização, executa K-Means e calcula itemsets/regras com FP-Growth. Não são apresentados resultados até ao primeiro clique em **Analisar Nichos**; o mesmo botão aplica alterações posteriores aos filtros. Os resultados são DataFrames de clusters, itemsets frequentes, regras de associação e dados analisados; o gráfico de dispersão é criado nativamente com Plotly.
450
460
 
451
- As dependências adicionais — `scikit-learn`, `mlxtend`, `plotly`, `seaborn`, `matplotlib` e `kagglehub` — são instaladas pelo procedimento normal de `npx`. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.2.77 install`; o instalador detecta e reutiliza o que já estiver válido.
461
+ As dependências adicionais — `scikit-learn`, `mlxtend`, `plotly`, `seaborn`, `matplotlib` e `kagglehub` — são instaladas pelo procedimento normal de `npx`. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.3.35 install`; o instalador detecta e reutiliza o que já estiver válido.
452
462
 
453
463
  ### Niche Finder Apify
454
464
 
package/README.md CHANGED
@@ -22,7 +22,7 @@ A primeira versão implementa a camada UI independente com:
22
22
  | Edição | Menu expansível abaixo de Automação com Limpador de Metadados, Clip Generator local em Cortes e Editor Python inspirado no PYEdit para vídeos e scripts locais |
23
23
  | Upload | YouTube via `youtube-automation-agent` adaptado internamente, fallback ordenado API Oficial → Upload directo → Postiz, upload de MP4 para Postiz via API key/MCP configurável, TikTok, Instagram e Facebook Pages no front end |
24
24
  | MCP | Catálogo local opcional de Short Video Maker, AutoVio, OpenMontage e OpenCut, com portas editáveis e activação |
25
- | Configurações | **Contas Google**, **Configuração API** e **Notificações**; API Keys divididas entre **Serviços e modelos** e **Fontes de materiais**, com várias chaves independentes por fonte, além de contas Google/YouTube por cartão, `INNERTUBE_API_KEY` global para todas as contas, providers, TTS, Nano Banana, Postiz, TikTok e Upload-Post |
25
+ | Configurações | **Contas Google**, **Configuração API** e **Notificações**; Notificações contém as subabas **Geral** e **Telegram Gateway**, com envio das mesmas operações locais para um Chat ID; API Keys divididas entre **Serviços e modelos** e **Fontes de materiais**, com várias chaves independentes por fonte, além de contas Google/YouTube por cartão, `INNERTUBE_API_KEY` global para todas as contas, providers, TTS, Nano Banana, Postiz, TikTok e Upload-Post |
26
26
  | Launcher | Execução via `npx`, instalação assistida, diagnóstico e preparação para distribuição |
27
27
 
28
28
  ## Upload-Post — publicação para múltiplas plataformas
@@ -56,7 +56,7 @@ No topo da área principal da aplicação existe o menu nativo de idioma no padr
56
56
 
57
57
  A UI suporta os temas **Dark** e **Light** através do menu nativo de três pontos do Streamlit, no local original do toolbar. Não existe um selector Theme adicional dentro da página. A configuração distribuída em `.streamlit/config.toml` define **Dark** como base inicial e o menu nativo continua responsável por alternar entre os modos, seguindo o padrão do [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo). O CSS próprio do Thunderbolt usa cores semânticas, `currentColor` e `color-mix` para acompanhar o tema activo, sem alterar a posição nem a funcionalidade do toolbar, do botão Deploy e do menu principal.
58
58
 
59
- ## Navegação da UI 0.2.89
59
+ ## Navegação da UI 0.3.35
60
60
 
61
61
  A barra lateral mantém os níveis principais, nesta ordem: **Início**, **Niche Finder**, **Pipeline**, **Pipeline TikTok**, **Automação**, **Edição**, **AI Influencers** e **Configurações**. **Pipeline** é expansível e contém **Criação de Vídeos**, **Criação de Músicas**, **Roteiros** e **Upload**. **Automação** também é expansível e contém **Automação Youtube**. **Edição** é expansível e contém **Limpador de Metadados**, **Cortes**, **Editor Python** e **Download Mídia**, nessa ordem. **AI Influencers** é expansível e contém **Personagens**, **Redes Sociais**, **Tutorial Meta** e **Tutorial Supabase**, nessa ordem. **Niche Finder** é expansível e contém **Niche Finder Kaggle** e **Niche Finder Apify**. **Configurações** é expansível e contém **Canais Youtube**, **Blueprints Youtube**, **MCP**, **Contas Google**, **Configuração API** e **Notificações**. O Início reúne o dashboard e as filas do Pipeline, sem botões de acções rápidas.
62
62
 
@@ -64,7 +64,7 @@ A subaba **Tutorial Supabase** apresenta o guia de criação das tabelas `plans`
64
64
 
65
65
  O conteúdo interno das páginas também é localizado, não apenas a navegação: títulos, subtítulos, descrições, labels de campos, placeholders, opções de selectores, botões, avisos, mensagens de sucesso/erro, estados vazios, métricas, expanderes e blocos Markdown/HTML são traduzidos no idioma seleccionado através da camada global de conteúdo. Valores técnicos, chaves de estado, IDs de formulários, nomes de ficheiros, URLs e dados introduzidos pelo utilizador permanecem inalterados.
66
66
 
67
- Todas as subabas internas também são localizadas pelo helper nativo de tabs: **Blueprints/Brandings**, **Pesquisa pública/Cadastro manual/Contas cadastradas**, **Upload/Biblioteca**, **Importar do YouTube/Canais em lote gmail/Cadastro manual**, **Criar vídeo/Vídeos**, **Novo roteiro/letra/Histórico guardado**, as três abas de análise de clusters, as quatro abas de cortes, **Vídeos/Código Python**, as quatro opções de Upload, **API Keys/Teste de vozes**, **Serviços e modelos/Fontes de materiais** e **Client MCP/Servidor MCP/Skill**. As chaves técnicas e a ordem funcional permanecem inalteradas.
67
+ Todas as subabas internas também são localizadas pelo helper nativo de tabs: **Blueprints/Brandings**, **Pesquisa pública/Cadastro manual/Contas cadastradas**, **Upload/Biblioteca**, **Importar do YouTube/Canais em lote gmail/Cadastro manual**, **Criar vídeo/Vídeos**, **Novo roteiro/letra/Histórico guardado**, as três abas de análise de clusters, as quatro abas de cortes, **Vídeos/Código Python**, as quatro opções de Upload, **API Keys/Teste de vozes**, **Serviços e modelos/Fontes de materiais**, **Client MCP/Servidor MCP/Skill** e **Notificações/Geral/Telegram**. As chaves técnicas e a ordem funcional permanecem inalteradas.
68
68
 
69
69
  Dentro de **Configurações > Contas Google**, a UI contém os cartões expansíveis de contas Google/YouTube, `sessionInfo` por conta, documentos de Upload directo, o formulário **Adicionar outra conta Gmail**, o bloco global de `INNERTUBE_API_KEY` e a configuração global do YouTube (OAuth Client ID, OAuth Client Secret e YouTube Data API Key). A página **Configuração API** contém as restantes credenciais, providers, modelos, serviços, Nano Banana, TikTok e Postiz.
70
70
 
@@ -72,7 +72,7 @@ Em **Configuração API > API Keys**, a subaba **Fontes de materiais** segue o p
72
72
 
73
73
  A subaba **Serviços e modelos** mantém as credenciais de providers e serviços que não são fontes de materiais. A chave `gemini_image_api_key` da Nano Banana continua separada de `gemini_api_key` do LLM textual. `INNERTUBE_API_KEY` permanece exclusivamente em **Configurações > Contas Google**, no bloco de configuração global fora dos cartões, e não é duplicada em API Keys.
74
74
 
75
- A página **AI Influencers > Tutorial Meta** apresenta o guia de configuração de uma conta Instagram profissional e das credenciais Meta para automações com n8n, distribuído localmente em `seed/references/guide-instagram.md` e com ligação para a [fonte original no GitHub](https://github.com/gyoridavid/ai_agents_az/blob/main/episode_8/guide-instagram.md). A página **Configurações > Notificações** mantém um histórico persistente de conclusões e falhas, reconcilia estados escritos por componentes locais e disponibiliza um checkbox independente para cada operação mapeada.
75
+ A página **AI Influencers > Tutorial Meta** apresenta o guia de configuração de uma conta Instagram profissional e das credenciais Meta para automações com n8n, distribuído localmente em `seed/references/guide-instagram.md` e com ligação para a [fonte original no GitHub](https://github.com/gyoridavid/ai_agents_az/blob/main/episode_8/guide-instagram.md). A página **Configurações > Notificações** mantém um histórico persistente de conclusões e falhas, reconcilia estados escritos por componentes locais e disponibiliza um checkbox independente para cada operação mapeada. A subaba **Geral** mantém esse centro local; a subaba **Telegram** permite configurar Bot Token, Chat ID e proxy opcional e envia as mesmas notificações pela Bot API oficial, sem polling ou mensagens recebidas.
76
76
 
77
77
  A página **Edição > Download Mídia** utiliza a API Python do [yt-dlp](https://github.com/yt-dlp/yt-dlp) para descarregar vídeos e áudio de URLs públicas, com qualidade, contentor, formato de áudio, legendas, metadados, playlists, progresso e histórico local em `storage/downloads/` e `storage/state/media_downloads.json`. Conversão e combinação de streams podem exigir FFmpeg.
78
78
 
@@ -193,9 +193,9 @@ thunderbolt
193
193
  No Windows PowerShell, se `npx` for bloqueado por `npx.ps1`, use directamente `npx.cmd`:
194
194
 
195
195
  ```powershell
196
- npx.cmd --yes @danhachuel/thunderbolt@0.2.77 install
197
- npx.cmd --yes @danhachuel/thunderbolt@0.2.77 doctor
198
- npx.cmd --yes @danhachuel/thunderbolt@0.2.77
196
+ npx.cmd --yes @danhachuel/thunderbolt@0.3.35 install
197
+ npx.cmd --yes @danhachuel/thunderbolt@0.3.35 doctor
198
+ npx.cmd --yes @danhachuel/thunderbolt@0.3.35
199
199
  ```
200
200
 
201
201
  Como alternativa, pode permitir scripts para o seu utilizador:
@@ -219,7 +219,7 @@ O menu expansível **Niche Finder** contém duas alternativas independentes. **N
219
219
 
220
220
  A interface apresenta dentro da aba os parâmetros da busca: número de clusters entre 2 e 10, suporte mínimo entre 0,01 e 0,50, país, categoria de engagement, intervalo de datas e tags. A página permanece sem resultados até o primeiro clique em **Analisar Nichos**; depois, se os parâmetros forem alterados, mostra os resultados anteriores e pede novo clique para aplicar os filtros actuais. O núcleo aplica normalização, filtros, `log1p`, `StandardScaler`, K-Means e FP-Growth. Os resultados aparecem em DataFrames para clusters, itemsets frequentes, regras de associação e dados analisados, acompanhados por uma visualização Plotly nativa e pesquisa de palavras nos clusters.
221
221
 
222
- **Niche Finder Apify** é a segunda alternativa e não usa o dataset, filtros, execução ou resultados Kaggle. Define três palavras-chave, período, limite de resultados, Shorts, duração, idioma de legendas e ordenação; depois de clicar em **Pesquisar no Apify**, inicia o actor `streamers~youtube-scraper`, acompanha o run, carrega o dataset, normaliza vídeos, limpa SRT, calcula VSC Ratio e tenta resumir as transcrições com o provider LLM configurado. Os resultados ficam na sessão própria `niche_apify_results`, o histórico pequeno fica em `storage/state/niche_apify_runs.json` e existem exportações JSON/CSV. Configure o **Apify API Token** em **Configuração API > API Keys > Serviços e modelos**. As dependências adicionais são instaladas pelo fluxo normal do pacote: `requests`, `pandas` e os componentes já existentes da análise. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.2.77 install`; o instalador detecta e reutiliza componentes já válidos.
222
+ **Niche Finder Apify** é a segunda alternativa e não usa o dataset, filtros, execução ou resultados Kaggle. Define três palavras-chave, período, limite de resultados, Shorts, duração, idioma de legendas e ordenação; depois de clicar em **Pesquisar no Apify**, inicia o actor `streamers~youtube-scraper`, acompanha o run, carrega o dataset, normaliza vídeos, limpa SRT, calcula VSC Ratio e tenta resumir as transcrições com o provider LLM configurado. Os resultados ficam na sessão própria `niche_apify_results`, o histórico pequeno fica em `storage/state/niche_apify_runs.json` e existem exportações JSON/CSV. Configure o **Apify API Token** em **Configuração API > API Keys > Serviços e modelos**. As dependências adicionais são instaladas pelo fluxo normal do pacote: `requests`, `pandas` e os componentes já existentes da análise. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.3.35 install`; o instalador detecta e reutiliza componentes já válidos.
223
223
 
224
224
  ## Editor Python baseado no PYEdit
225
225
 
package/app/main.py CHANGED
@@ -42,7 +42,7 @@ from hermes_ui.music import list_music_files, materialize_suno_audio, request_su
42
42
  from hermes_ui.media_downloader import AUDIO_FORMATS, VIDEO_CONTAINERS, VIDEO_QUALITY_OPTIONS, MediaDownloadError, build_download_options, clear_media_download_history, dependency_status, download_media, list_media_downloads, media_download_file
43
43
  from hermes_ui.notifications import clear_notifications, list_notifications, mark_all_notifications_read, mark_notification_read, notification_event_catalog, notification_preferences, record_notification, reconcile_persisted_notifications, save_notification_preferences, unread_notification_count
44
44
  from hermes_ui.languages import LANGUAGE_CODES, VIDEO_LANGUAGE_CODES, LANGUAGE_FLAG_DATA_URIS, language_code, language_label, ui_language_menu_label, ui_text, video_language_label, video_language_options
45
- from hermes_ui.api_key_tests import test_apify_credentials, test_kaggle_credentials, test_material_source_credentials, test_nano_banana_credentials, test_postiz_credentials, test_tiktok_credentials, test_upload_post_credentials, test_voice_provider
45
+ from hermes_ui.api_key_tests import test_apify_credentials, test_kaggle_credentials, test_material_source_credentials, test_nano_banana_credentials, test_postiz_credentials, test_telegram_credentials, test_tiktok_credentials, test_upload_post_credentials, test_voice_provider
46
46
  from hermes_ui.tutorials import tutorial_body, tutorial_caption, tutorial_title
47
47
 
48
48
  from hermes_ui.script_documents import list_script_documents, read_script_document, save_script_document, script_storage_path
@@ -4955,93 +4955,166 @@ def render_settings():
4955
4955
  st.warning("A amostra de voz anterior não é um ficheiro de áudio legível e foi removida do estado local. Teste a voz novamente.")
4956
4956
 
4957
4957
 
4958
+ def _render_telegram_notification_settings() -> None:
4959
+ """Render Telegram outbound notification settings and a safe diagnostics action."""
4960
+ settings = read_json("settings.json", {})
4961
+ if not isinstance(settings, dict):
4962
+ settings = {}
4963
+ st.subheader("Telegram Gateway")
4964
+ st.caption("Envie para o Telegram as mesmas notificações que aparecem na subaba Geral. A integração usa a Bot API oficial e não recebe mensagens nem inicia polling.")
4965
+ with st.form("telegram_notifications_form"):
4966
+ telegram_enabled = st.checkbox(
4967
+ "Activar notificações Telegram",
4968
+ value=bool(settings.get("telegram_enabled", False)),
4969
+ help="Quando activo, cada nova notificação permitida em Geral é enviada para o Chat ID configurado.",
4970
+ )
4971
+ telegram_cols = st.columns(2)
4972
+ with telegram_cols[0]:
4973
+ telegram_bot_token = st.text_input(
4974
+ "Telegram Bot Token",
4975
+ value=str(settings.get("telegram_bot_token") or ""),
4976
+ type="password",
4977
+ help="Token criado pelo BotFather. Fica guardado apenas no storage local.",
4978
+ )
4979
+ _render_credential_status(telegram_bot_token)
4980
+ with telegram_cols[1]:
4981
+ telegram_chat_id = st.text_input(
4982
+ "Telegram Chat ID",
4983
+ value=str(settings.get("telegram_chat_id") or ""),
4984
+ help="ID do utilizador, grupo ou canal que receberá as notificações; também pode ser um username aceite pelo Telegram.",
4985
+ )
4986
+ telegram_proxy_url = st.text_input(
4987
+ "Proxy Telegram (opcional)",
4988
+ value=str(settings.get("telegram_proxy_url") or ""),
4989
+ help="Proxy HTTP/HTTPS/SOCKS suportado pelo ambiente, quando o acesso directo ao Telegram não estiver disponível.",
4990
+ )
4991
+ telegram_timeout_seconds = st.number_input(
4992
+ "Timeout de envio (segundos)",
4993
+ min_value=5,
4994
+ max_value=120,
4995
+ value=int(settings.get("telegram_timeout_seconds", 15) or 15),
4996
+ step=5,
4997
+ )
4998
+ st.caption("O teste consulta getMe para validar o Bot Token e não envia uma mensagem de teste.")
4999
+ _render_api_test_control(
5000
+ settings,
5001
+ "telegram",
5002
+ lambda: test_telegram_credentials(telegram_bot_token, telegram_chat_id),
5003
+ widget_key="api_test_telegram",
5004
+ )
5005
+ if st.form_submit_button("Guardar configuração Telegram", type="primary", use_container_width=True):
5006
+ settings.update({
5007
+ "telegram_enabled": bool(telegram_enabled),
5008
+ "telegram_bot_token": telegram_bot_token.strip(),
5009
+ "telegram_chat_id": telegram_chat_id.strip(),
5010
+ "telegram_proxy_url": telegram_proxy_url.strip(),
5011
+ "telegram_timeout_seconds": int(telegram_timeout_seconds),
5012
+ })
5013
+ write_json("settings.json", settings)
5014
+ st.success("Configuração Telegram guardada no storage local.")
5015
+ st.rerun()
5016
+
5017
+ if telegram_enabled and telegram_bot_token.strip() and telegram_chat_id.strip():
5018
+ st.success("Telegram está preparado para receber novas notificações.")
5019
+ elif telegram_enabled:
5020
+ st.warning("Telegram está activo, mas ainda falta configurar o Bot Token e o Chat ID.")
5021
+ else:
5022
+ st.info("Telegram está desactivado. As notificações continuam disponíveis na subaba Geral.")
5023
+
5024
+
4958
5025
  def render_notifications():
4959
5026
  st.title("Notificações")
4960
5027
  st.caption("Centro de notificações internas persistentes do Thunderbolt. As conclusões são guardadas no storage local e aparecem quando a aplicação é actualizada.")
4961
- reconcile_persisted_notifications()
4962
- preferences = notification_preferences()
4963
- catalog = notification_event_catalog()
4964
- notifications = list_notifications(limit=500)
4965
- unread_count = unread_notification_count()
4966
- summary_cols = st.columns(3)
4967
- with summary_cols[0]:
4968
- st.metric("Não lidas", unread_count)
4969
- with summary_cols[1]:
4970
- st.metric("Total guardado", len(notifications))
4971
- with summary_cols[2]:
4972
- st.metric("Operações mapeadas", len(catalog))
4973
-
4974
- action_cols = st.columns([1.4, 1.4, 2.2])
4975
- with action_cols[0]:
4976
- if st.button("Marcar todas como lidas", use_container_width=True, disabled=unread_count == 0):
4977
- mark_all_notifications_read()
4978
- st.rerun()
4979
- with action_cols[1]:
4980
- if st.button("Actualizar notificações", use_container_width=True):
4981
- reconcile_persisted_notifications()
4982
- st.rerun()
4983
- with action_cols[2]:
4984
- confirm_clear = st.checkbox("Confirmar limpeza do histórico", key="confirm_clear_notifications")
4985
- if st.button("Limpar histórico", use_container_width=True, disabled=not confirm_clear):
4986
- clear_notifications()
4987
- st.session_state.pop("confirm_clear_notifications", None)
4988
- st.rerun()
5028
+ general_tab, telegram_tab = render_localized_tabs(["Geral", "Telegram"])
4989
5029
 
4990
- st.divider()
4991
- st.subheader("Operações notificadas")
4992
- st.caption("Ligue ou desligue cada tipo de notificação. As preferências ficam guardadas no storage local e aplicam-se às próximas conclusões.")
4993
- grouped: dict[str, list[dict[str, str]]] = {}
4994
- for event in catalog:
4995
- grouped.setdefault(event["category"], []).append(event)
4996
- with st.form("notification_preferences_form"):
4997
- pending_preferences: dict[str, bool] = {}
4998
- for category, events in grouped.items():
4999
- st.markdown(f"**{category}**")
5000
- for event in events:
5001
- pending_preferences[event["code"]] = st.checkbox(
5002
- event["label"],
5003
- value=bool(preferences.get(event["code"], True)),
5004
- help=event["description"],
5005
- key=f"notification_preference_{event['code']}",
5006
- )
5007
- if st.form_submit_button("Guardar preferências", type="primary", use_container_width=True):
5008
- save_notification_preferences(pending_preferences)
5009
- st.success("Preferências de notificação guardadas.")
5010
- st.rerun()
5030
+ with general_tab:
5031
+ reconcile_persisted_notifications()
5032
+ preferences = notification_preferences()
5033
+ catalog = notification_event_catalog()
5034
+ notifications = list_notifications(limit=500)
5035
+ unread_count = unread_notification_count()
5036
+ summary_cols = st.columns(3)
5037
+ with summary_cols[0]:
5038
+ st.metric("Não lidas", unread_count)
5039
+ with summary_cols[1]:
5040
+ st.metric("Total guardado", len(notifications))
5041
+ with summary_cols[2]:
5042
+ st.metric("Operações mapeadas", len(catalog))
5043
+
5044
+ action_cols = st.columns([1.4, 1.4, 2.2])
5045
+ with action_cols[0]:
5046
+ if st.button("Marcar todas como lidas", use_container_width=True, disabled=unread_count == 0):
5047
+ mark_all_notifications_read()
5048
+ st.rerun()
5049
+ with action_cols[1]:
5050
+ if st.button("Actualizar notificações", use_container_width=True):
5051
+ reconcile_persisted_notifications()
5052
+ st.rerun()
5053
+ with action_cols[2]:
5054
+ confirm_clear = st.checkbox("Confirmar limpeza do histórico", key="confirm_clear_notifications")
5055
+ if st.button("Limpar histórico", use_container_width=True, disabled=not confirm_clear):
5056
+ clear_notifications()
5057
+ st.session_state.pop("confirm_clear_notifications", None)
5058
+ st.rerun()
5011
5059
 
5012
- st.divider()
5013
- st.subheader("Histórico de notificações")
5014
- filter_cols = st.columns([1, 1.4])
5015
- category_options = ["Todas"] + sorted({event["category"] for event in catalog})
5016
- with filter_cols[0]:
5017
- selected_category = st.selectbox("Categoria", category_options, key="notifications_category_filter")
5018
- with filter_cols[1]:
5019
- selected_state = st.selectbox("Estado", ["Todas", "Não lidas", "Lidas"], key="notifications_state_filter")
5020
- category_filter = "" if selected_category == "Todas" else selected_category
5021
- unread_filter = selected_state == "Não lidas"
5022
- filtered = list_notifications(limit=500, category=category_filter, unread_only=unread_filter)
5023
- if selected_state == "Lidas":
5024
- filtered = [item for item in filtered if item.get("read")]
5025
- if not filtered:
5026
- st.info("Ainda não existem notificações para os filtros seleccionados.")
5027
- for item in filtered:
5028
- with st.container(border=True):
5029
- notification_cols = st.columns([3.3, 1.4, 1])
5030
- with notification_cols[0]:
5031
- st.write(f"**{item.get('title') or item.get('label') or 'Notificação'}**")
5032
- st.caption(item.get("message") or "")
5033
- metadata = item.get("metadata") if isinstance(item.get("metadata"), dict) else {}
5034
- if metadata:
5035
- public_details = " · ".join(f"{key}: {value}" for key, value in metadata.items() if value not in (None, ""))
5036
- if public_details:
5037
- st.caption(public_details)
5038
- with notification_cols[1]:
5039
- st.caption(f"{item.get('category', 'Sistema')} · {item.get('created_at', '—')}")
5040
- st.write("Lida" if item.get("read") else "Não lida")
5041
- with notification_cols[2]:
5042
- if not item.get("read") and st.button("Marcar como lida", key=f"mark_notification_{item.get('id')}", use_container_width=True):
5043
- mark_notification_read(str(item.get("id")))
5044
- st.rerun()
5060
+ st.divider()
5061
+ st.subheader("Operações notificadas")
5062
+ st.caption("Ligue ou desligue cada tipo de notificação. As preferências ficam guardadas no storage local e aplicam-se às próximas conclusões.")
5063
+ grouped: dict[str, list[dict[str, str]]] = {}
5064
+ for event in catalog:
5065
+ grouped.setdefault(event["category"], []).append(event)
5066
+ with st.form("notification_preferences_form"):
5067
+ pending_preferences: dict[str, bool] = {}
5068
+ for category, events in grouped.items():
5069
+ st.markdown(f"**{category}**")
5070
+ for event in events:
5071
+ pending_preferences[event["code"]] = st.checkbox(
5072
+ event["label"],
5073
+ value=bool(preferences.get(event["code"], True)),
5074
+ help=event["description"],
5075
+ key=f"notification_preference_{event['code']}",
5076
+ )
5077
+ if st.form_submit_button("Guardar preferências", type="primary", use_container_width=True):
5078
+ save_notification_preferences(pending_preferences)
5079
+ st.success("Preferências de notificação guardadas.")
5080
+ st.rerun()
5081
+
5082
+ st.divider()
5083
+ st.subheader("Histórico de notificações")
5084
+ filter_cols = st.columns([1, 1.4])
5085
+ category_options = ["Todas"] + sorted({event["category"] for event in catalog})
5086
+ with filter_cols[0]:
5087
+ selected_category = st.selectbox("Categoria", category_options, key="notifications_category_filter")
5088
+ with filter_cols[1]:
5089
+ selected_state = st.selectbox("Estado", ["Todas", "Não lidas", "Lidas"], key="notifications_state_filter")
5090
+ category_filter = "" if selected_category == "Todas" else selected_category
5091
+ unread_filter = selected_state == "Não lidas"
5092
+ filtered = list_notifications(limit=500, category=category_filter, unread_only=unread_filter)
5093
+ if selected_state == "Lidas":
5094
+ filtered = [item for item in filtered if item.get("read")]
5095
+ if not filtered:
5096
+ st.info("Ainda não existem notificações para os filtros seleccionados.")
5097
+ for item in filtered:
5098
+ with st.container(border=True):
5099
+ notification_cols = st.columns([3.3, 1.4, 1])
5100
+ with notification_cols[0]:
5101
+ st.write(f"**{item.get('title') or item.get('label') or 'Notificação'}**")
5102
+ st.caption(item.get("message") or "")
5103
+ metadata = item.get("metadata") if isinstance(item.get("metadata"), dict) else {}
5104
+ if metadata:
5105
+ public_details = " · ".join(f"{key}: {value}" for key, value in metadata.items() if value not in (None, ""))
5106
+ if public_details:
5107
+ st.caption(public_details)
5108
+ with notification_cols[1]:
5109
+ st.caption(f"{item.get('category', 'Sistema')} · {item.get('created_at', '—')}")
5110
+ st.write("Lida" if item.get("read") else "Não lida")
5111
+ with notification_cols[2]:
5112
+ if not item.get("read") and st.button("Marcar como lida", key=f"mark_notification_{item.get('id')}", use_container_width=True):
5113
+ mark_notification_read(str(item.get("id")))
5114
+ st.rerun()
5115
+
5116
+ with telegram_tab:
5117
+ _render_telegram_notification_settings()
5045
5118
 
5046
5119
 
5047
5120
  def render_models_ai_tutorial():
@@ -213,6 +213,18 @@ def test_postiz_credentials(api_key: str, base_url: str = "https://api.postiz.co
213
213
  return _get(f"{base}/integrations", headers={"Authorization": api_key})
214
214
 
215
215
 
216
+ def test_telegram_credentials(bot_token: str, chat_id: str) -> dict[str, Any]:
217
+ """Validate the Telegram bot token with getMe without sending a message."""
218
+ bot_token = str(bot_token or "").strip()
219
+ chat_id = str(chat_id or "").strip()
220
+ if not bot_token or not chat_id:
221
+ return _missing("Introduza o Bot Token e o Chat ID Telegram antes de testar.")
222
+ result = _get(f"https://api.telegram.org/bot{bot_token}/getMe")
223
+ if result.get("ok"):
224
+ result["message"] = "Bot Token Telegram válido. O Chat ID foi aceite localmente; o teste não enviou mensagem."
225
+ return result
226
+
227
+
216
228
  def test_material_source_credentials(provider: str, api_key: str) -> dict[str, Any]:
217
229
  """Run a read-only provider-specific check for material source cards."""
218
230
  provider = str(provider or "").strip().lower()
@@ -614,6 +614,27 @@ for _language_code, _tab_translation in TAB_TRANSLATIONS.items():
614
614
  UI_TRANSLATIONS[_language_code].update(_tab_translation)
615
615
 
616
616
 
617
+ _NOTIFICATION_TAB_TRANSLATIONS: dict[str, dict[str, str]] = {
618
+ "pt": {"Geral": "Geral", "Telegram": "Telegram"},
619
+ "en": {"Geral": "General", "Telegram": "Telegram"},
620
+ "zh": {"Geral": "常规", "Telegram": "Telegram"},
621
+ "de": {"Geral": "Allgemein", "Telegram": "Telegram"},
622
+ "vi": {"Geral": "Chung", "Telegram": "Telegram"},
623
+ "tr": {"Geral": "Genel", "Telegram": "Telegram"},
624
+ "ru": {"Geral": "Общие", "Telegram": "Telegram"},
625
+ "es": {"Geral": "General", "Telegram": "Telegram"},
626
+ "id": {"Geral": "Umum", "Telegram": "Telegram"},
627
+ "it": {"Geral": "Generale", "Telegram": "Telegram"},
628
+ "pl": {"Geral": "Ogólne", "Telegram": "Telegram"},
629
+ "ga": {"Geral": "Ginearálta", "Telegram": "Telegram"},
630
+ "ar": {"Geral": "عام", "Telegram": "Telegram"},
631
+ "he": {"Geral": "כללי", "Telegram": "Telegram"},
632
+ }
633
+ for _language_code, _notification_tab_translation in _NOTIFICATION_TAB_TRANSLATIONS.items():
634
+ if _language_code in UI_TRANSLATIONS:
635
+ UI_TRANSLATIONS[_language_code].update(_notification_tab_translation)
636
+
637
+
617
638
  TUTORIAL_TRANSLATIONS: dict[str, dict[str, str]] = {
618
639
  "pt": {"Tutorial Supabase": "Tutorial Supabase", "Guia de configuração do Supabase para automações com n8n.": "Guia de configuração do Supabase para automações com n8n."},
619
640
  "en": {"Tutorial Supabase": "Supabase Tutorial", "Guia de configuração do Supabase para automações com n8n.": "Supabase setup guide for n8n automations."},
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
5
6
  import re
6
7
  import uuid
7
8
  from datetime import datetime, timezone
@@ -40,6 +41,8 @@ EVENT_CATALOG: tuple[dict[str, str], ...] = (
40
41
  {"code": "mcp_operation_completed", "category": "Integrações", "label": "Operação MCP concluída", "description": "Quando uma operação mutável de integração MCP terminar com sucesso."},
41
42
  )
42
43
  EVENTS_BY_CODE = {item["code"]: item for item in EVENT_CATALOG}
44
+ LOGGER = logging.getLogger(__name__)
45
+
43
46
  SENSITIVE_MARKERS = (
44
47
  "token",
45
48
  "secret",
@@ -119,6 +122,24 @@ def _history() -> list[dict[str, Any]]:
119
122
  return [item for item in saved if isinstance(item, dict)]
120
123
 
121
124
 
125
+ def _dispatch_telegram_notification(entry: dict[str, Any]) -> None:
126
+ """Deliver a persisted event to Telegram without affecting local flows."""
127
+ try:
128
+ settings = storage.read_json("settings.json", {})
129
+ if not isinstance(settings, dict) or not bool(settings.get("telegram_enabled", False)):
130
+ return
131
+ from integrations.telegram_gateway import send_notification_to_telegram
132
+
133
+ result = send_notification_to_telegram(entry, settings)
134
+ if not result.ok:
135
+ error_type = result.data.get("error_type") if isinstance(result.data, dict) else ""
136
+ if error_type != "missing_configuration":
137
+ LOGGER.warning("Telegram notification delivery failed (%s).", error_type or "unknown_error")
138
+ except Exception as exc:
139
+ # External notification delivery must never break production, uploads or UI.
140
+ LOGGER.warning("Telegram notification delivery failed (%s).", type(exc).__name__)
141
+
142
+
122
143
  def record_notification(
123
144
  event_type: str,
124
145
  title: str,
@@ -151,6 +172,7 @@ def record_notification(
151
172
  "dedupe_key": str(dedupe_key or ""),
152
173
  }
153
174
  storage.write_json(NOTIFICATIONS_FILE, [entry, *history][:MAX_NOTIFICATIONS])
175
+ _dispatch_telegram_notification(entry)
154
176
  return entry
155
177
 
156
178
 
@@ -96,6 +96,11 @@ DEFAULTS: dict[str, Any] = {
96
96
  "youtube_client_secret": "",
97
97
  "youtube_batch_accounts": [],
98
98
  "youtube_batch_selected_account_id": "",
99
+ "telegram_enabled": False,
100
+ "telegram_bot_token": "",
101
+ "telegram_chat_id": "",
102
+ "telegram_proxy_url": "",
103
+ "telegram_timeout_seconds": 15,
99
104
  "notification_preferences": {
100
105
  "video_completed": True,
101
106
  "music_completed": True,
@@ -0,0 +1,158 @@
1
+ """Outbound Telegram Gateway integration for persisted Thunderbolt notifications."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any, Callable
7
+
8
+ import requests
9
+
10
+ from .platforms import IntegrationResult
11
+
12
+ DEFAULT_TELEGRAM_API_BASE_URL = "https://api.telegram.org"
13
+ TELEGRAM_MAX_MESSAGE_LENGTH = 4096
14
+ DEFAULT_TELEGRAM_TIMEOUT = 15
15
+
16
+
17
+ class TelegramGatewayAdapter:
18
+ """Send Thunderbolt notifications through the Telegram Bot API.
19
+
20
+ The adapter deliberately implements outbound delivery only. It does not
21
+ poll Telegram or receive messages, which keeps notification delivery
22
+ isolated from the production and upload flows.
23
+ """
24
+
25
+ def __init__(self, settings: dict[str, Any] | None = None, *, request_client: Any = requests) -> None:
26
+ self.settings = settings or {}
27
+ self.enabled = bool(self.settings.get("telegram_enabled", False))
28
+ self.bot_token = str(self.settings.get("telegram_bot_token") or os.getenv("TELEGRAM_BOT_TOKEN", "")).strip()
29
+ self.chat_id = str(self.settings.get("telegram_chat_id") or os.getenv("TELEGRAM_CHAT_ID", "")).strip()
30
+ self.proxy_url = str(self.settings.get("telegram_proxy_url") or os.getenv("TELEGRAM_PROXY_URL", "")).strip()
31
+ self.timeout = max(1, int(self.settings.get("telegram_timeout_seconds", DEFAULT_TELEGRAM_TIMEOUT) or DEFAULT_TELEGRAM_TIMEOUT))
32
+ self._request_client = request_client
33
+
34
+ def _error(self, message: str, *, status_code: int | None = None, error_type: str = "") -> IntegrationResult:
35
+ data: dict[str, Any] = {}
36
+ if status_code is not None:
37
+ data["status_code"] = status_code
38
+ if error_type:
39
+ data["error_type"] = error_type
40
+ return IntegrationResult(False, message, data)
41
+
42
+ def status(self) -> IntegrationResult:
43
+ if not self.enabled:
44
+ return self._error("Telegram está desactivado nas notificações.", error_type="disabled")
45
+ if not self.bot_token or not self.chat_id:
46
+ return self._error("Telegram não está configurado: indique o Bot Token e o Chat ID.", error_type="missing_configuration")
47
+ return IntegrationResult(True, "Telegram configurado para notificações.", {"chat_id_configured": True})
48
+
49
+ def _url(self, method: str) -> str:
50
+ return f"{DEFAULT_TELEGRAM_API_BASE_URL}/bot{self.bot_token}/{method}"
51
+
52
+ def _request_kwargs(self, payload: dict[str, Any]) -> dict[str, Any]:
53
+ kwargs: dict[str, Any] = {"json": payload, "timeout": self.timeout}
54
+ if self.proxy_url:
55
+ kwargs["proxies"] = {"http": self.proxy_url, "https": self.proxy_url}
56
+ return kwargs
57
+
58
+ def _post(self, method: str, payload: dict[str, Any]) -> IntegrationResult:
59
+ try:
60
+ response = self._request_client.post(self._url(method), **self._request_kwargs(payload))
61
+ except requests.RequestException as exc:
62
+ return self._error("Não foi possível contactar o Telegram.", error_type=type(exc).__name__)
63
+ except Exception as exc:
64
+ return self._error("A chamada ao Telegram falhou.", error_type=type(exc).__name__)
65
+
66
+ status_code = int(getattr(response, "status_code", 0) or 0)
67
+ if status_code == 429:
68
+ return self._error("O Telegram limitou a chamada de notificação; tente novamente mais tarde.", status_code=status_code, error_type="rate_limited")
69
+ if status_code in {401, 403}:
70
+ return self._error("O Telegram rejeitou o Bot Token ou não permite o envio para este Chat ID.", status_code=status_code, error_type="unauthorized")
71
+ if status_code < 200 or status_code >= 300:
72
+ return self._error("O Telegram rejeitou a notificação.", status_code=status_code or None, error_type="http_error")
73
+ try:
74
+ payload_response = response.json()
75
+ except (TypeError, ValueError):
76
+ return self._error("O Telegram devolveu uma resposta inválida.", status_code=status_code, error_type="invalid_json")
77
+ if not isinstance(payload_response, dict) or payload_response.get("ok") is not True:
78
+ return self._error("O Telegram não aceitou a notificação.", status_code=status_code, error_type="api_error")
79
+ return IntegrationResult(True, "Notificação enviada pelo Telegram.", {"status_code": status_code, "result": payload_response.get("result")})
80
+
81
+ @staticmethod
82
+ def split_message(text: str, *, max_length: int = TELEGRAM_MAX_MESSAGE_LENGTH) -> list[str]:
83
+ """Split a message at line boundaries while respecting Telegram's limit."""
84
+ normalized = str(text or "").strip()
85
+ if not normalized:
86
+ return []
87
+ chunks: list[str] = []
88
+ remaining = normalized
89
+ while len(remaining) > max_length:
90
+ cut = remaining.rfind("\n", 0, max_length + 1)
91
+ if cut < max_length // 2:
92
+ cut = max_length
93
+ chunks.append(remaining[:cut].rstrip())
94
+ remaining = remaining[cut:].lstrip()
95
+ if remaining:
96
+ chunks.append(remaining)
97
+ return chunks
98
+
99
+ @staticmethod
100
+ def format_notification(notification: dict[str, Any]) -> str:
101
+ """Format an already-redacted local notification for Telegram."""
102
+ title = str(notification.get("title") or notification.get("label") or "Notificação").strip()
103
+ message = str(notification.get("message") or "").strip()
104
+ category = str(notification.get("category") or "Sistema").strip()
105
+ created_at = str(notification.get("created_at") or "").strip()
106
+ lines = [title]
107
+ if message:
108
+ lines.append(message)
109
+ details: list[str] = []
110
+ if category:
111
+ details.append(f"Categoria: {category}")
112
+ if created_at:
113
+ details.append(f"Data: {created_at}")
114
+ metadata = notification.get("metadata")
115
+ if isinstance(metadata, dict):
116
+ for key, value in metadata.items():
117
+ if value in (None, "", [], {}):
118
+ continue
119
+ details.append(f"{key}: {value}")
120
+ if details:
121
+ lines.extend(["", *details])
122
+ return "\n".join(lines)
123
+
124
+ def send_message(self, text: str) -> IntegrationResult:
125
+ """Send plain text to the configured chat, splitting oversized messages."""
126
+ status = self.status()
127
+ if not status.ok:
128
+ return status
129
+ chunks = self.split_message(text)
130
+ if not chunks:
131
+ return self._error("A notificação Telegram está vazia.", error_type="empty_message")
132
+ message_ids: list[str] = []
133
+ for chunk in chunks:
134
+ result = self._post("sendMessage", {"chat_id": self.chat_id, "text": chunk})
135
+ if not result.ok:
136
+ return result
137
+ message_result = result.data.get("result") if isinstance(result.data, dict) else None
138
+ if isinstance(message_result, dict) and message_result.get("message_id") is not None:
139
+ message_ids.append(str(message_result["message_id"]))
140
+ return IntegrationResult(True, "Notificação enviada pelo Telegram.", {"message_ids": message_ids, "chunks": len(chunks)})
141
+
142
+ def send_notification(self, notification: dict[str, Any]) -> IntegrationResult:
143
+ """Format and send one persisted Thunderbolt notification."""
144
+ return self.send_message(self.format_notification(notification))
145
+
146
+
147
+ def send_notification_to_telegram(notification: dict[str, Any], settings: dict[str, Any] | None = None) -> IntegrationResult:
148
+ """Convenience function used by the notification persistence layer."""
149
+ return TelegramGatewayAdapter(settings).send_notification(notification)
150
+
151
+
152
+ __all__ = [
153
+ "DEFAULT_TELEGRAM_API_BASE_URL",
154
+ "DEFAULT_TELEGRAM_TIMEOUT",
155
+ "TELEGRAM_MAX_MESSAGE_LENGTH",
156
+ "TelegramGatewayAdapter",
157
+ "send_notification_to_telegram",
158
+ ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.3.34",
3
+ "version": "0.3.35",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",
package/requirements.txt CHANGED
@@ -1,5 +1,5 @@
1
1
  streamlit>=1.37,<2
2
- requests>=2.31,<3
2
+ requests[socks]>=2.31,<3
3
3
  pandas>=2.0,<3
4
4
  imageio-ffmpeg>=0.6,<1
5
5
  toml>=0.10,<1