Repository navigation
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/
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. »
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. »
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. »
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.
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. »
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 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 |
| 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
|
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.
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
postMessageAPI (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.
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) whosesubismetanews; a request carryingX-Real-IPorX-Forwarded-Forhas come through nginx and is refused with 403. Bad or missing token: 401 with no detail. -
Limits:
ttl_srequired, 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 (httpsvignette_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/majand RSS/JSON at the deadline; its permalink answers410 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.
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.
-
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 forhttp(s)(neverjavascript:).?demo=1and?embed=1are 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.
# 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.
| 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 |
- MODULES-EN · Architecture · API-Reference
- ToolBox 👁️ (WAF transparent qui route le vhost)
- SBX-OS · Cartes-du-Hall (the Billets card) · Messagerie
-
WEBUI-PANEL-GUIDELINES.md(main repository) — look & feel of the admin panels
SecuBox-Deb · Licence : CMSD-1.0 (Source-Disclosed)
Contact : CyberMind · Gérald Kerma · Notre-Dame-du-Cruet, Savoie
Hardware-Matrix · Acknowledgments · Wiki v2.5.0
SecuBox | FR | DE | 中文 | v3.0.0-alpha.12
- Home
- Architecture
- Trixie | FR
- Hardware-Matrix
- Roadmap
- Profils
- Modules-by-Profile 📦 lite / isp / full : ce qu'embarque chaque profil | FR
- Modules-by-Family 🗂️ les modules par famille | FR
- DNS-Filtrage
- Multiboot ⭐
-
Live-USB-VirtualBox
run-vbox.sh -
Live-USB-QEMU
run-qemu.sh - Live-USB | FR | DE | 中文
- Auto-Load 🚚 boxes sans écran | FR
- First-Boot-Premier-Pas 🧭 premier pas | FR
- Installation | FR | DE | 中文
- ARM-Installation | FR | DE | 中文
- Netboot-Install 🔧 MOCHAbin netboot + signed install
- ESPRESSObin | FR | DE | 中文
- Eye-Remote 📡
- Android-ToolBox 📱 one-tap R3
- Browser-Extension 🧩 cartographie
- QEMU-ARM64 🖥️
- Configuration | FR | DE | 中文
- Troubleshooting | FR | DE | 中文
- ToolBox 👁️ cabine numérique · rapport vie privée
- Anti-Track 🛡️ bloque · empoisonne · anonymise
- Billets 📮 micro-blog gateway · média vignette zoomable
- Messagerie 💬 mur public, messages privés, radio/Billets/BBS centralisés
- Identite-SBX-OS 🪪 personnes, appareils, comptes liés, connexion passive
- Metalogue 🕸️ OSINT suite · Maigret + SpiderFoot (Maltego-style)
- WAF 🛡️ pare-feu applicatif autonome (nft direct, hors-HTTP) | FR
- SOC 🎯 vues rassemblées | FR
- Actor-Intelligence 🕵️ acteurs, échelle de réponse | FR
- NAC 🔐 contrôle d'accès réseau | FR
- Webfilter 🧹 filtrage DNS par catégories et par appareil | FR
- Interceptor 🎯 supervision de l'inspection (sbxmitm, sbxwaf, nDPI) | FR
- Network-Modes 🌐 modes réseau, WAN, interfaces | FR
- Caches-and-Mirrors 💾 caches de la box et dépôt APT local | FR
- Portal 🚪 portail de session | FR
- Certificates 📜 certificats | FR
- Metrics 📊 mesures, sites, cartes, rapport PDF | FR
- SBX-OS 🧩 le Hall en application | FR
- Aurora-Design 🎨 design system admin | FR
- Admin-Panels-Gallery 🖼️ galerie des 37 pages | FR
- ThreatMesh 🛰️ blocklist souveraine (feeds + mesh, sans CAPI) | FR
- Modules 🇬🇧
- MODULES-FR 🇫🇷
- MODULES-DE 🇩🇪
- MODULES-ZH 🇨🇳
-
Operations-Runbook 🛠️ incidents | FR
-
API-Reference | FR | DE | 中文
- Multi-Agent-Worktree — un agent · une issue · une branche