diff --git a/README.md b/README.md index 5a4be0c..b741508 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,41 @@ # Flux RSS - Commune de Silly -Genere deux flux RSS 2.0 (actualites + agenda) pour la commune de Silly -(Belgique), a partir de l'API JSON REST (Plone / `imio.smartweb`) utilisee -par www.silly.be — sans navigateur headless. +Genere trois flux RSS/Atom pour la commune de Silly (Belgique) : actualites, +agenda et page Facebook. Deux implementations equivalentes sont fournies : -Voir [silly_rss.py](silly_rss.py) pour le detail technique (empreinte -Cloudflare, contrainte `b_size=20`, etc.). +- **[php/](php/)** — a deployer sur un hebergement PHP mutualise classique. + **Recommande** : aucun cron ni conf serveur a ecrire, chaque flux se + regenere a la demande (avec cache 15 min) des qu'un lecteur RSS l'interroge. +- **[silly_rss.py](silly_rss.py)** — script Python autonome a lancer via cron, + pour un serveur ou vous controlez le cron/nginx (ex. Yunohost). -## Utilisation manuelle +Les deux utilisent la meme source : l'API JSON REST (Plone / `imio.smartweb`) +que www.silly.be charge en JS via les balises `` / +`` — pas besoin de navigateur headless. Le detail technique +(empreinte Cloudflare, contrainte `b_size=20`) est commente dans +[php/common.php](php/common.php) et [silly_rss.py](silly_rss.py). -```bash -python3 silly_rss.py /chemin/de/sortie -``` +## Option recommandee : PHP (hebergement perso) -Genere `actualites.xml` et `agenda.xml` dans le dossier indique (`.` par -defaut). Aucune dependance externe : Python 3.10+ (stdlib) et `curl` -(present par defaut sur Yunohost). +1. Copier tout le dossier [php/](php/) sur l'hebergement (FTP/SFTP), par ex. + dans `silly-rss/` a la racine du site. +2. Verifier que PHP a l'extension `curl` activee (quasi toujours le cas en + mutualise standard). +3. Le dossier `php/cache/` doit etre inscriptible par PHP (permissions par + defaut suffisent generalement sur du mutualise ; sinon `chmod 775`). +4. Les flux sont alors accessibles en : + - `https://votre-site/silly-rss/actualites.php` + - `https://votre-site/silly-rss/agenda.php` + - `https://votre-site/silly-rss/facebook.php` (proxy/cache du flux + [FetchRSS](https://fetchrss.com) de la page Facebook, pour avoir une + URL stable sur votre propre domaine) +5. Ajouter ces 3 URLs comme sources dans FreshRSS (Administration > Flux + RSS > Ajouter un flux). -## Deploiement sur Yunohost +Teste avec PHP 8.3 CLI : les 3 scripts produisent du XML valide avec de +vraies donnees (40 actualites, 12 evenements, flux Facebook proxifie). + +## Option alternative : Python + cron (Yunohost) 1. **Dossier de sortie** @@ -30,12 +48,15 @@ defaut). Aucune dependance externe : Python 3.10+ (stdlib) et `curl` ligne a ajouter via `crontab -e` (execution toutes les heures). 3. **nginx** — voir [deploy/nginx-silly-rss.conf](deploy/nginx-silly-rss.conf), - a copier dans `/etc/nginx/conf.d/.d/silly-rss.conf` puis : + a copier dans `/etc/nginx/conf.d/akbar.nohost.me.d/silly-rss.conf` puis : ```bash sudo nginx -t && sudo systemctl reload nginx ``` + NB : `/rss` est deja pris par FreshRSS sur ce domaine, d'ou le choix de + `/silly-rss/` comme chemin. + 4. **Premiere generation** (avant le premier passage du cron) : ```bash @@ -43,13 +64,18 @@ defaut). Aucune dependance externe : Python 3.10+ (stdlib) et `curl` ``` 5. **FreshRSS** — ajouter comme sources : - - `https:///silly-rss/actualites.xml` - - `https:///silly-rss/agenda.xml` + - `https://akbar.nohost.me/silly-rss/actualites.xml` + - `https://akbar.nohost.me/silly-rss/agenda.xml` + +Cette option ne gere pas Facebook (pas de proxy) : ajouter directement +l'URL FetchRSS dans FreshRSS si vous utilisez cette variante. ## Facebook (page communedesilly) -Pas de solution fiable trouvee : le HTML de facebook.com est vide de -contenu exploitable sans authentification, et l'instance RSS-Bridge testee -(`FacebookBridge`) renvoie une erreur 400. Recommandation : un service -tiers specialise (RSS.app, Fetch RSS) plutot qu'un scraper maison, qui -sera cassé au premier changement de Facebook. +Pas de solution fiable trouvee en scraping direct : le HTML de +facebook.com est vide de contenu exploitable sans authentification, et +l'instance RSS-Bridge testee (`FacebookBridge`) renvoie une erreur 400. +Solution retenue : [FetchRSS](https://fetchrss.com) (service tiers). La +variante PHP le reproxifie sous votre propre domaine (`facebook.php`) pour +avoir 3 URLs stables et partageables ; la variante Python s'appuie +directement sur l'URL FetchRSS. diff --git a/php/.gitignore b/php/.gitignore new file mode 100644 index 0000000..626a6cf --- /dev/null +++ b/php/.gitignore @@ -0,0 +1 @@ +cache/*.xml diff --git a/php/actualites.php b/php/actualites.php new file mode 100644 index 0000000..aed86da --- /dev/null +++ b/php/actualites.php @@ -0,0 +1,50 @@ += ($data['items_total'] ?? 0)) { + break; + } + } + return $items; +} + +function build_news_feed(): string +{ + $items = []; + foreach (fetch_all_news() as $item) { + $id = $item['id'] ?? ''; + $summary = strip_html($item['description_fr'] ?? $item['description'] ?? null); + $items[] = [ + 'title' => $item['title_fr'] ?? $item['title'] ?? '(sans titre)', + 'link' => SITE . '/actualites?news=' . rawurlencode($id), + 'guid' => $item['UID'] ?? $id, + 'pubDate' => $item['effective'] ?? $item['created'] ?? null, + 'description' => $summary, + ]; + } + return build_rss( + 'Silly.be - Actualités', + SITE . '/actualites', + 'Actualités de la commune de Silly (Belgique)', + $items + ); +} + +serve_cached_feed('actualites', 'build_news_feed'); diff --git a/php/agenda.php b/php/agenda.php new file mode 100644 index 0000000..4e68771 --- /dev/null +++ b/php/agenda.php @@ -0,0 +1,73 @@ += ($data['items_total'] ?? 0)) { + break; + } + } + return $items; +} + +function format_event_meta(array $item): string +{ + $parts = []; + $start = !empty($item['start']) ? new DateTime($item['start']) : null; + $end = !empty($item['end']) ? new DateTime($item['end']) : null; + + if ($start) { + $parts[] = !empty($item['whole_day']) + ? 'Le ' . $start->format('d/m/Y') + : 'Le ' . $start->format('d/m/Y à H:i'); + } + if ($end && (!$start || $end->format('Y-m-d') !== $start->format('Y-m-d'))) { + $parts[] = "jusqu'au " . $end->format('d/m/Y H:i'); + } + $address = implode(', ', array_filter([$item['street'] ?? null, $item['zipcode'] ?? null, $item['city'] ?? null])); + if ($address) { + $parts[] = "Lieu : $address"; + } + return implode(' — ', $parts); +} + +function build_events_feed(): string +{ + $items = []; + foreach (fetch_all_events() as $item) { + $id = basename(rtrim($item['@id'] ?? '', '/')); + $summary = strip_html($item['description_fr'] ?? $item['description'] ?? null); + $meta = format_event_meta($item); + $description = implode(' | ', array_filter([$meta, $summary])); + $items[] = [ + 'title' => $item['title_fr'] ?? $item['title'] ?? '(sans titre)', + 'link' => SITE . '/agenda?event=' . rawurlencode($id), + 'guid' => $item['UID'] ?? $id, + 'pubDate' => $item['created'] ?? $item['start'] ?? null, + 'description' => $description, + ]; + } + return build_rss( + 'Silly.be - Agenda', + SITE . '/agenda', + "Agenda / événements de la commune de Silly (Belgique)", + $items + ); +} + +serve_cached_feed('agenda', 'build_events_feed'); diff --git a/php/cache/.gitkeep b/php/cache/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/php/common.php b/php/common.php new file mode 100644 index 0000000..b08f543 --- /dev/null +++ b/php/common.php @@ -0,0 +1,137 @@ + true, + CURLOPT_USERAGENT => USER_AGENT, + CURLOPT_HTTPHEADER => array_merge(['Accept: application/json'], $extraHeaders), + CURLOPT_TIMEOUT => 20, + CURLOPT_FOLLOWLOCATION => true, + ]); + $body = curl_exec($ch); + $errno = curl_errno($ch); + $error = curl_error($ch); + $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($errno !== 0) { + throw new RuntimeException("curl error fetching $url: $error"); + } + if ($status >= 400) { + throw new RuntimeException("HTTP $status fetching $url"); + } + return $body; +} + +/** + * Sert un contenu XML depuis un cache fichier, en le regenerant via + * $builder() si le cache est absent ou perime. Evite de solliciter + * silly.be / fetchrss.com a chaque poll de chaque abonne FreshRSS. + */ +function serve_cached_feed(string $cacheKey, callable $builder): void +{ + header('Content-Type: application/rss+xml; charset=utf-8'); + + if (!is_dir(CACHE_DIR)) { + mkdir(CACHE_DIR, 0775, true); + } + $cacheFile = CACHE_DIR . '/' . $cacheKey . '.xml'; + + if (is_file($cacheFile) && (time() - filemtime($cacheFile)) < CACHE_TTL_SECONDS) { + readfile($cacheFile); + return; + } + + try { + $xml = $builder(); + file_put_contents($cacheFile, $xml); + echo $xml; + } catch (Throwable $e) { + // En cas d'erreur (site source indisponible, etc.), on sert le + // cache perime plutot que de casser le flux chez les abonnes. + if (is_file($cacheFile)) { + readfile($cacheFile); + return; + } + http_response_code(502); + header('Content-Type: text/plain; charset=utf-8'); + echo "Erreur de generation du flux : " . $e->getMessage(); + } +} + +function strip_html(?string $text): string +{ + if (!$text) { + return ''; + } + $text = preg_replace('/<[^>]+>/', ' ', $text); + $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + return trim(preg_replace('/\s+/', ' ', $text)); +} + +function rfc822(?string $isoDate): string +{ + try { + $dt = $isoDate ? new DateTime($isoDate) : new DateTime('now', new DateTimeZone('UTC')); + } catch (Exception $e) { + $dt = new DateTime('now', new DateTimeZone('UTC')); + } + return $dt->format(DateTime::RSS); +} + +function xml_escape(string $text): string +{ + return htmlspecialchars($text, ENT_XML1 | ENT_QUOTES, 'UTF-8'); +} + +/** + * Construit un flux RSS 2.0 complet a partir d'une liste d'items deja + * formates (chacun un tableau title/link/guid/pubDate/description). + */ +function build_rss(string $title, string $link, string $description, array $items): string +{ + $now = rfc822(null); + $xml = '' . "\n"; + $xml .= '' . "\n \n"; + $xml .= ' ' . xml_escape($title) . "\n"; + $xml .= ' ' . xml_escape($link) . "\n"; + $xml .= ' ' . xml_escape($description) . "\n"; + $xml .= " fr-be\n"; + $xml .= " $now\n"; + + foreach ($items as $item) { + $xml .= " \n"; + $xml .= ' ' . xml_escape($item['title']) . "\n"; + $xml .= ' ' . xml_escape($item['link']) . "\n"; + $xml .= ' ' . xml_escape($item['guid']) . "\n"; + $xml .= ' ' . rfc822($item['pubDate'] ?? null) . "\n"; + $xml .= ' ' . xml_escape($item['description']) . "\n"; + $xml .= " \n"; + } + + $xml .= " \n\n"; + return $xml; +} diff --git a/php/facebook.php b/php/facebook.php new file mode 100644 index 0000000..6c77afe --- /dev/null +++ b/php/facebook.php @@ -0,0 +1,43 @@ +getMessage(); +}