Skip to content

Billets

CyberMind-FR edited this page Oct 12, 2026 · 3 revisions

Billets 📮

EN | FR | 🟣 MIND | 🔐 self-hosted

Publie court, embarque, republie — un micro-blog passerelle inter-médias, hébergé chez toi.

Billets is a self-hosted micro-blog gateway: short posts with restricted Markdown, inline media galleries (zoomable), social embeds (oEmbed), emoji reactions, moderated comments, and Atom/JSON feeds. FastAPI + aiosqlite (WAL) + Jinja2 on its own vhost (billets.<board>.secubox.in), behind nginx → sbxwaf → HAProxy. Every billet is versioned in Gitea; every security decision is written to an append-only BLAKE2b-chained event log.

  • Public feed: https://billets.<board>.secubox.in/
  • Author admin: …/admin · Operator panel: admin.<board>.secubox.in/billets/

🎯 Cas d'usage

1️⃣ Micro-blog souverain (short-form)

Publie des notes courtes en Markdown restreint (liens, gras, listes — pas de HTML brut). Chaque billet a un permalien stable, un slug lisible, et entre dans les flux Atom/JSON. Brouillon → publication → archivage, avec historique complet des révisions dans Gitea (git log --follow sur billets/<id>.md).

« Je veux un journal public que je contrôle, sans plateforme tierce. »

2️⃣ Galerie média avec vignette zoomable 🖼️

Joins plusieurs images à un billet (png · jpeg · webp · gif, ≤ 5 Mo). Chaque upload est entièrement ré-encodé (Pillow) : EXIF/GPS supprimés, polyglottes neutralisés, SVG refusé, garde anti-decompression-bomb. Le public voit une grille de vignettes ; un clic ouvre une lightbox zoomable avec navigation clavier ◀▶ (dégradation gracieuse : sans JS, le lien ouvre l'image). La première image alimente og:image / twitter:image pour les aperçus de partage.

« Je poste une photo en vignette, cliquable et agrandissable, à côté du texte. »

3️⃣ Passerelle inter-médias sociaux (embed / republish)

Colle une URL YouTube / Vimeo / Mastodon / PeerTube / Bluesky… : billets résout l'oEmbed (SSRF-gardé, HTML sanitizé, frame-src en allow-list) et intègre le média inline. En sortie, un endpoint oEmbed + des share intents (Mastodon, copie de lien) permettent de republier le billet ailleurs.

« J'embarque une vidéo externe, et je peux republier mon billet vers Mastodon. »

4️⃣ Commentaires modérés + réactions emoji

Le public réagit (👍 ❤️ 😂 😮 😢 🔥) et commente ; les commentaires passent par une file de modération (anti-spam honeypot + jeton temporel + rate-limit, IP jamais stockée en clair — BLAKE2b). L'auteur approuve/rejette depuis l'admin.

5️⃣ Backup portable .sbxsite 💾

Un clic (/admin/export.sbxsite) produit un seul fichier contenant tous les billets et leurs médias en base64 — réimportable ailleurs. Le contenu voyage avec le fichier ; pas de dossier média à trimballer séparément.

« J'exporte tout mon blog, images comprises, en un fichier réimportable. »

6️⃣ Console opérateur + reset mot de passe 🔑

Le panneau admin.<board>.secubox.in/billets/ (surface opérateur SecuBox) donne accès à toutes les surfaces admin (tableau de bord, nouveau billet, modération, backup, flux) et à un override mot de passe : si l'auteur oublie son mot de passe, l'opérateur (root) déclenche /admin/override, gardé par le secret module /etc/secubox/secrets/billets, qui génère un mot de passe fort affiché une seule fois (journalisé, rate-limité).


🎞️ The immersive feed (0.9 → 0.11)

The public feed is a living, immersive stream: each billet is a card, and a card with media opens into a full "scene" (theatre) with subtitles-style comments overlaid.

Behaviour Detail
Sorted by activity a published or commented billet rises to the top (bumped_at: publication or last approved comment; it never goes back). RSS/JSON feeds keep publication order
Auto refresh GET /feed/maj?depuis=<mark> returns the cards that moved since the client's last mark; the page polls every 15 s and on tab return, adds new cards on top without reloading, keeps the reader's scroll position, never removes a billet being read
Animations a new billet drops in with a halo and a "new" badge, a bumped one slides from its old place with a "commented" badge; prefers-reduced-motion is respected
Resume opening a permalink resumes exactly where you were (position kept on page leave, tab hidden, every 4 s while playing)
Settings kept the "subtitles" and "CRT" toggles are remembered
Comments published as soon as sent (CSRF, honeypot, signed form token and rate limit still apply); a visitor with a SecuBox session has the session name replace whatever they typed; up to 8 000 characters

📎 Embeds, link cards and local thumbnails

Source Behaviour
PeerTube (the box's own instance) loads immediately, nothing leaves the box
YouTube sovereign mode: the iframe is armed but not loaded; the click is the consent. Before it, the reader sees a local snapshot. URLs are rewritten to youtube-nocookie.com, the banner reads "YouTube · click to load". Thumbnails are relayed by the box (/yt-vignette?v=<id>), only for videos a billet references
oEmbed (Vimeo, Mastodon, Bluesky…) resolved SSRF-guarded, HTML sanitised, frame-src allow-list
Link card for a URL with no working oEmbed: a local HTML card built from OpenGraph (title, description, https image), never an iframe or remote HTML
Images re-encoded, 480 px thumbnail, zoomable in the billet view and in the body text (one lightbox, Esc and arrow keys)
Hosted videos (mp4, webm) a thumbnail extracted by ffmpeg at 1 s (fallback: first frame) is stored as <id>_t.jpg; the feed and the Hall card show a preview instead of a placeholder. Without ffmpeg, or if extraction fails: no thumbnail, no error. Backfill: python -m api.manage backfill-posters

Hosted video played in the scene

A billet whose attachment is a video shows its thumbnail with a ▶; a click plays it full frame on a black background in the scene. Its position is the real currentTime and follows play and pause.

Comments anchored to a moment

A comment can be tied to an instant of the media:

  • hosted video: the real playback position;
  • YouTube: the real position, read through the player's postMessage API (current time, play/pause), both on the billet page and in the feed theatre;
  • otherwise: an estimate ("base + elapsed time"), capped at 3 hours, then "unknown": the line is sent without an instant rather than with a false one, and nothing is saved locally when the instant is unknown.

⏳ Ephemeral billets and MetaNews

MetaNews turns forming topics into short-lived billets.

  • Door: POST /service/ephemere, on the module's Unix socket only. The caller presents a service token (HS256) whose sub is metanews; a request carrying X-Real-IP or X-Forwarded-For has come through nginx and is refused with 403. Bad or missing token: 401 with no detail.
  • Limits: ttl_s required, from 30 s to 1 h (never a durable billet); create only, no edit or delete; a ceiling of 60 per hour on the billets side, whatever the caller does.
  • MetaNews side: a topic covered by at least 2 sources and updated recently goes into the feed for 5 minutes by default, then disappears. At most 6 topics per rolling hour, a one-hour grace delay per topic, most important first, old news never replayed. A billets outage stops the round without marking the topic and never stops polling.
  • Content: the body lists up to 5 sources as links (https only, de-duplicated); the most recent main article is sent as embed_url, so billets builds the preview, and the topic's image (https vignette_url) is fetched locally as the thumbnail, because news sites refuse page-capture robots.
  • Life cycle: an expired billet leaves the feed, /feed/suite, /feed/maj and RSS/JSON at the deadline; its permalink answers 410 Gone ("this ephemeral billet has expired"); a sweep archives it (kept 24 h for audit) then deletes it. Each ephemeral card shows a countdown (⏳ m:ss, red and blinking the last minute) computed from the seconds left at render time, so the device clock does not matter.

🧵 Publishing from the BBS

The BBS can publish a thread as a billet. Billets knows nothing about the BBS (the BBS calls billets, never the reverse), and:

  • a local thread is refused: make it public explicitly first, since publishing puts it on the internet;
  • publication runs under the operator's SecuBox session, not under an identity of the BBS;
  • local messages of a public thread are not forwarded; images are re-encoded (EXIF stripped);
  • republishing updates the existing billet instead of creating another one; unpublishing removes it.

🪟 Aurora portal and admin look (0.12.0)

  • Operator portal /billets/: redone in the Aurora style, in French, on the real statistics of the box's blog (stats.json): tiles, latest billets, reactions and administration links. If the blog cannot be reached, the page shows "—" and says so. Links are only set for http(s) (never javascript:). ?demo=1 and ?embed=1 are available.
  • Blog administration dressed in Aurora: dashboard, moderation, editing, login and password reset use a shared stylesheet (billets-admin.css), with the same classes, templates and routes as before; a system font stack replaces the remote font. The public feed keeps its own identity.

🟢 Prise en main (ROOT)

# 1. Installer le paquet
apt install secubox-billets            # crée le venv + pip install (Pillow inclus)

# 2. Créer l'auteur admin
cd /usr/lib/secubox/billets
sudo -u secubox venv/bin/python -m api.manage create-author admin

# 3. Router le vhost par le WAF (jamais de bypass)
haproxyctl vhost add billets.<board>.secubox.in
# + route sbxwaf: /etc/secubox/waf/haproxy-routes.json  ->  ["127.0.0.1", 8910]
systemctl restart secubox-billets

Écrire un billet : …/admin/billets/new → texte Markdown + éventuelle URL (référence ou embed) + images (champ 🖼️). Publier.


🔒 Posture sécurité

Contrôle Mise en œuvre
Upload ré-encodage from-pixels (EXIF/polyglot strip), SVG refusé, cap 5 Mo avant traitement, dimensions bornées
Média servi /media/ nginx statique, img-src 'self' — aucun tiers, aucune relaxation CSP
Auth argon2id (hors boucle), session signée itsdangerous, TOTP optionnel, double-submit CSRF, rate-limit par vrai client (X-Forwarded-For)
Override secret opérateur root-only, comparaison constant-time (bytes), mot de passe affiché une fois (jamais en URL), audité
Boucle partagée tout le CPU/IO (Pillow, base64, export) hors event-loop (asyncio.to_thread) — pas de SPOF board-wide
Journal event-log append-only chaîné BLAKE2b ; révisions Gitea par billet

See also

SecuBox | FR | DE | 中文 | v3.0.0-alpha.12


Projet

Soutenir


🔴 BOOT — Démarrer

🟢 ROOT — Configuration

🟣 MIND — Modules

🔵 MESH — Référence

🟠 WALL — Matériel

🤖 Workflow Agents


Liens

Clone this wiki locally