Aggregatore e archivio OSINT self-hosted. Recupera feed RSS/Atom a intervalli regolari, ne estrae il testo completo e lo indicizza per la ricerca full-text. Sopra l'archivio: un bollettino quotidiano ordinato per tipo di fonte, allerte sulle ricerche salvate, classificazione delle fonti con il codice dell'Ammiragliato, annotazioni e tag, favoriti con note, catture visive a pagina intera della pagina originale. Multi-utente con ruoli. Pensato per macchine piccole: raccolta a intervalli lunghi e a bassa priorità, poche scritture su disco (vedi «Carico e scritture»).
Stack: PHP 8.1+ e SQLite per la webapp, Python 3.11+ per fetcher e worker. Nessun framework, nessun database server, nessuna build.
| Percorso | Ruolo |
|---|---|
webapp/bollettino.php |
pagina d'ingresso: la giornata per tipo di fonte, allerte, salute dei feed |
webapp/novita.php |
corrispondenze delle allerte, da segnare come viste |
webapp/search.php, browse.php, item.php |
ricerca FTS, lettura cronologica, dettaglio articolo (filtri per categoria di fonte) |
webapp/feeds.php |
gestione feed, classificazione e grado di affidabilità, import/export |
webapp/favorites.php, notes.php, capture.php |
favoriti con note, annotazioni, catture visive |
webapp/stats.php, accessi.php, users.php, theme.php |
statistiche, log accessi, utenti e ruoli, tema grafico |
fetcher/rssintel_fetch.py |
scarica i feed abilitati, estrae il testo con trafilatura, popola DB e indice FTS5 |
fetcher/rssintel_watch.py |
allerte: riesegue le ricerche salvate sui nuovi articoli dopo ogni raccolta |
fetcher/rssintel_capture.py, cdp.py, shot.py |
catture visive via Chromium headless |
fetcher/rssintel_rebuild_fts.py |
ricostruzione integrale dell'indice FTS dai testi estratti |
bot/rssintel_bot.py, rssintel_core.py |
bot Telegram (in costruzione a lotti, vedi sotto) |
schema.sql |
schema completo del database SQLite |
deploy/ |
.htaccess, header di sicurezza, unit systemd, migrazioni |
config.sample.php |
modello di configurazione |
feeds— sorgenti RSS/Atom, stato dell'ultimo fetch, categoria e affidabilità della fonteitems— articoli: metadati + percorso del testo estratto (text_path) +content_hashitems_fts— indice FTS5 autonomo (title,body,link,feed): conserva il testo, cosi'snippet()produce gli estratti evidenziatiannotations/tags/annotation_tags— note e tag per articolousers— utenti e ruoli (reader,collaborator,admin)saved_searches/watch_hits— ricerche salvate, allerte e loro corrispondenzefavorites— favoriti per utente, con nota personalecaptures— catture visive (i file stanno fuori dalla webroot)access_log/login_attempts/ip_geo_cache— log accessi e tentativi di loginsite_settings— impostazioni globali (tema)tg_links/tg_link_codes/tg_watch_prefs— account Telegram collegati, codici di collegamento, preferenze delle sentinelle (le crea il bot)
git clone https://github.com/Indr1d-C0ld/RSSIntel.git
cd RSSIntel
# 1. Configurazione
cp config.sample.php config.php
$EDITOR config.php # percorso DB, conservazione del log accessi
# 2. Database
mkdir -p /var/lib/rssintel/text
sqlite3 /var/lib/rssintel/rssintel.db < schema.sql
# 3. Fetcher
python3 -m venv /opt/rssintel-venv
/opt/rssintel-venv/bin/pip install -r fetcher/requirements.txt
# 4. Webapp: servi la cartella webapp/ con PHP (Apache/nginx/php -S) avendo cura
# che config.php sia raggiungibile un livello sopra la document root, oppure
# copialo dentro webapp/. Proteggi l'accesso (vedi deploy/htaccess.sample).Adatta e installa le unit di esempio (il timer raccoglie ogni 3 ore, a priorità minima di CPU e disco):
cp deploy/rssintel-fetch.service.sample /etc/systemd/system/rssintel-fetch.service
cp deploy/rssintel-fetch.timer.sample /etc/systemd/system/rssintel-fetch.timer
systemctl enable --now rssintel-fetch.timerIl fetcher legge i percorsi da variabili d'ambiente (RSSINTEL_DB,
RSSINTEL_TXT_DIR, RSSINTEL_RAW_DIR), con /var/lib/rssintel come
predefinito, e l'user-agent da RSSINTEL_UA: mettici l'indirizzo del tuo sito.
Tre ore bastano per la maggior parte dei feed, che tengono gli articoli
dell'ultimo giorno o più; per un feed che ne tiene solo poche ore conviene un
intervallo più breve, altrimenti qualche articolo esce dal feed prima del giro.
Bollettino (bollettino.php, pagina d'ingresso). La giornata in una pagina,
ordinata per tipo di fonte invece che per ora: in testa le corrispondenze delle
allerte, poi gli articoli delle fonti specialistiche, istituzionali e d'analisi,
in fondo e compressi il flusso generalista e le fonti non classificate. In coda la
salute dei feed: errori, feed non aggiornati da ore e feed silenziosi rispetto
al proprio ritmo — si segnala una settimana senza articoli solo quando, al ritmo
abituale della fonte (stimato sui 23 giorni precedenti), avrebbe avuto meno del 5%
di probabilita'. Un feed che risponde sempre 304 senza errori, ma ha smesso di
pubblicare, altrimenti non lo segnalerebbe nessuno.
Allerte (novita.php). Una ricerca salvata con l'allerta attiva viene
rieseguita dopo ogni raccolta sui soli articoli nuovi
(fetcher/rssintel_watch.py, avviato da ExecStartPost nella unit del fetcher:
vedi deploy/rssintel-fetch.service.sample). Le corrispondenze compaiono in
«Novità», con un contatore nella navigazione; «visto» e' un comando esplicito.
All'attivazione non si carica lo storico. Il riferimento d'avanzamento e'
items.fetched_at, aggiornato anche quando un articolo viene reindicizzato.
Classificazione delle fonti (feeds.php). Per ogni feed: categoria (tipo di
testata: generalista, specialistica, istituzionale, analisi, blog, advocacy — non
l'orientamento) e affidabilita' secondo il codice dell'Ammiragliato (A–F), con
una nota di motivazione. La lettera valuta la fonte; la cifra 1–6 del codice,
che valuta la singola informazione, non appartiene al feed e non e' gestita qui.
Categoria e grado compaiono come distintivo («B · specialistica») e sono filtri in
Lettura e Ricerca.
Aggiornando un'installazione esistente vedi deploy/migrations/.
Da un articolo (o dai Favoriti) un utente collaborator/admin può chiedere
una cattura a pagina intera della pagina originale: un JPEG (altezza massima
12.000 px, di solito qualche centinaio di KB) con miniatura,
impronta SHA-256, URL finale dopo i redirect e data. Le catture sono versionate
(ricatturare aggiunge, non sostituisce) ed eliminabili da chi le ha richieste o
da un admin.
- La pagina accoda soltanto e crea un file-segnale
(
<captures_dir>/.richiesta). Un'unità.pathdi systemd lo vede (inotify, nessun polling) e avviafetcher/rssintel_capture.py, che svuota la coda, una cattura alla volta (deploy/rssintel-capture.{service,path}.sample). Senza richieste Chromium non parte mai. - Prima dello scatto si nascondono i banner di consenso più diffusi (OneTrust, Didomi, Usercentrics, Quantcast, Cookiebot, Google Funding Choices, Sourcepoint, iubenda…).
- Il lavoro passa dallo stesso filtro anti-SSRF del fetcher: solo http/https e solo indirizzi pubblici, redirect rivalidati a ogni salto.
chromium --screenshotcattura solo il viewport: la pagina intera si ottiene via protocollo DevTools (Page.captureScreenshotconcaptureBeyondViewport), con un client minimo infetcher/cdp.py(solo libreria standard).- I file stanno fuori dalla webroot (
captures_dir, default<cartella del db>/captures) e li servewebapp/capture.phpdopo l'autenticazione.
Requisiti: chromium e Pillow nel Python che esegue il worker.
bot/rssintel_bot.py porta RSSIntel sul telefono senza passare dal portale:
un servizio Python separato (python-telegram-bot 22.8, long polling, nessuna
porta in ingresso) che legge e scrive lo stesso database. Lo si costruisce a
lotti; questa versione (L5) collega l'account, cerca, fa leggere, gestisce le
sentinelle e ne manda le notifiche, e porta il bollettino del giorno.
-
Ricerca. Si scrive una parola, oppure
/cerca.- I risultati sono gli stessi di
search.php: stessa query FTS, stesso ordine per data, stesso filtro per categoria. Lo verifica un confronto con il portale. - 6 risultati per pagina, con fonte, grado, data, dominio ed estratto evidenziato.
- Se FTS rifiuta la query (
l'Iran,Al-Aqsa,iran-israel), il bot la riprova con le parole tra virgolette e dice come l'ha interpretata. - Gli estratti si calcolano solo per la pagina mostrata, e una query troppo larga si interrompe dopo 8 secondi.
- I risultati sono gli stessi di
-
Lettura. Ogni risultato apre una scheda: estratto, «Apri fonte», «Link» da copiare e il testo completo, in messaggi da circa 3.500 caratteri oppure come file
.txtse è lungo. -
Sentinelle.
/segui parolecrea un'allerta sui nuovi articoli.- Prima di crearla mostra quanti ne avrebbe trovati negli ultimi 14 giorni, con la stessa condizione del worker, e suggerisce la modalita': istantanea, riepilogo oppure solo Novità.
- Si puo' dare una scadenza, utile per seguire un evento.
- E' la stessa ricerca salvata del portale: compare con 🔔 su
search.phpe la rieseguerssintel_watch.py. /allertele gestisce;/novitamostra le segnalazioni non viste. «Visto» e' la stessa colonna dinovita.php.- Le sentinelle da proporre al primo collegamento si leggono da un file
indicato da
RSSINTEL_BOT_SUGGEST_FILE, una per riga:Nome | query | istantanea|riepilogo|novita.
-
Notifiche.
- ⚡ Istantanee: un messaggio per giro di raccolta. Al massimo un suono all'ora e otto al giorno; le altre arrivano silenziose.
- 📅 Riepiloghi silenziosi agli orari di
RSSINTEL_BOT_DIGEST_AT(predefiniti 08:00, 13:00, 19:00). - Niente invii nelle ore di
RSSINTEL_BOT_QUIET(02:00-08:00). - Cio' che e' gia' stato visto sul portale non viene mandato.
- La coda sta nel database di stato del bot, non in quello condiviso; la consegna e' «almeno una volta», con i reinvii marcati.
- Per ogni sentinella si puo' scegliere «solo conteggi»: niente nomi ne' titoli in chat.
-
Bollettino.
/oggi(oppureieri, o una data) mostra la giornata comebollettino.php: stesse sezioni, stessi limiti e stessa salute delle fonti, con allerte e sezioni da sfogliare.- Facoltativo: il bollettino di ieri ogni mattina, in silenzio (orario in
RSSINTEL_BOT_BULLETIN_AT). - Gli amministratori ricevono un avviso quando una fonte va in errore, smette di aggiornarsi o di pubblicare, e quando torna regolare.
- Facoltativo: il bollettino di ieri ogni mattina, in silenzio (orario in
-
Bottoni. Portano solo un gettone corto: la query vera resta nel database di stato del bot, legata all'account e valida 48 ore.
-
Segreti. Token e ID Telegram autorizzati arrivano come credenziali
systemd(LoadCredentialEncrypted=,LoadCredential=), mai da variabili d'ambiente né daconfig.php. La libreria scrive il token in chiaro nel messaggio d'errore quando Telegram lo rifiuta: il bot lo maschera sul testo finale di ogni riga di log, tracce comprese, ed esce con codice 78, che la unit non riavvia. -
Accesso. Solo gli ID in lista bianca ricevono risposta; gli altri non causano nemmeno un accesso al database. L'account si collega a un utente RSSIntel con un codice monouso (15 minuti, conservato solo come impronta); utente, ruolo e disattivazione si rileggono dal database a ogni comando. Solo chat private: aggiunto a un gruppo, il bot esce. Ogni utente può scollegare il proprio account dal Profilo.
-
Database condiviso. Il bot gira con un utente proprio nel gruppo del server web. La cartella del database deve essere setgid e scrivibile dal gruppo (2775) e il database 664, così chi apre per primo crea
-wal/-shmutilizzabili da tutti. Niente sticky bit: confs.protected_regular=2(predefinito su Debian) il kernel impedirebbe al server web di aprire i file creati dal bot, e il database gli diventerebbe di sola lettura. Connessione per operazione, nessuna transazione di lettura lasciata aperta.
useradd --system --gid www-data --home-dir /nonexistent --no-create-home \
--shell /usr/sbin/nologin rssintel-bot
chmod 2775 /var/lib/rssintel && chmod 664 /var/lib/rssintel/rssintel.db
install -d -o root -g root -m 755 /opt/rssintel-bot # codice di root: non modificabile dal server web
install -m 644 bot/rssintel_bot.py bot/rssintel_core.py bot/requirements.txt /opt/rssintel-bot/
python3 -m venv /opt/rssintel-bot/venv
/opt/rssintel-bot/venv/bin/pip install --require-hashes -r /opt/rssintel-bot/requirements.txt
systemd-ask-password -n "Token:" | \
systemd-creds encrypt --with-key=host --name=bot_token - /etc/credstore.encrypted/rssintel-bot.token
(umask 077; echo "<ID Telegram>" > /etc/credstore/rssintel-bot.allowed)
cp deploy/rssintel-bot.service.sample /etc/systemd/system/rssintel-bot.service
systemctl daemon-reload && systemctl enable --now rssintel-bot
# collegamento: codice monouso, poi /start CODICE al bot
sudo -u rssintel-bot env RSSINTEL_DB=/var/lib/rssintel/rssintel.db \
/opt/rssintel-bot/venv/bin/python /opt/rssintel-bot/rssintel_bot.py codice <utente>La unit di esempio è blindata (ProtectSystem=strict, solo la cartella del
database scrivibile, testi e catture in sola lettura). Il gruppo del server web
può leggere anche file di altri siti: elencali in un'estensione della unit
(InaccessiblePaths=) e in RSSINTEL_BOT_HIDDEN, che il bot verifica all'avvio.
| Chiave | Significato |
|---|---|
db_path |
percorso assoluto del file SQLite |
access_log_retention_days |
giorni di conservazione del log accessi (0 = per sempre) |
ipgeo |
risolve il paese degli IP del log accessi via ip-api.com |
captures_dir, text_dir |
cartelle delle catture e dei testi, se non accanto al DB |
admins |
legacy, non più necessario con la tabella users |
RSSIntel gira bene anche su una macchina piccola con un solo SSD:
- Raccolta ogni 3 ore, con
Nice=19eIOSchedulingClass=idle; un giro di una decina di feed dura pochi secondi se nessuno ha novità. - Stato dei feed (ultimo controllo, esito, ETag) scritto in una sola
transazione a fine giro; un feed che risponde
304non scrive nulla da sé. Un solo checkpoint del WAL per giro. - Indice di ricerca: ogni articolo nuovo vi entra una volta sola, già con il testo (nessun trigger che inserisce una riga vuota da riscrivere).
- Testi compressi (
.txt.gz), uno per articolo; l'HTML grezzo non si salva (SAVE_RAW_HTML = False). - Catture solo su richiesta, in JPEG; nessun timer.
- Log accessi: niente righe per le miniature e i controlli automatici
delle catture; conservazione impostabile (
access_log_retention_days).
La traduzione automatica (proxy verso un motore locale tipo NLLB) è stata tolta: un modello in locale pesa troppo per questo tipo di macchina.
GPL-3.0-or-later — vedi LICENSE.