runv-server

server tooling for runv.club
Log | Files | Refs | README

commit 68da7eaf39410852228c36bab58a6e69a3ea67cd
parent 2a936140614eb0b29fb6abcf2340a790cb5dbada
Author: Pablo Murad <pablo@pablomurad.com>
Date:   Tue, 19 May 2026 21:09:24 -0300

motd

Diffstat:
Mdocs/05-tools-and-system-experience.md | 45++++++++++++++++++++++++++++++++++++++++++++-
Mdocs/17-community-commands.md | 5+++--
Mtools/bin/runv-help | 10++++++++++
Mtools/bin/runv-who | 10++++++++--
Mtools/lib/runv_community.py | 26++++++++++++++++++++++----
Mtools/motd/60-runv | 52+++++++++++++++++++++++++++++-----------------------
6 files changed, 116 insertions(+), 32 deletions(-)

diff --git a/docs/05-tools-and-system-experience.md b/docs/05-tools-and-system-experience.md @@ -8,7 +8,7 @@ 1. Pacotes APT listados em `tools/manifests/apt_packages.txt` (alias `chat` → metapacote `weechat`). O manifesto inclui **`weechat-curses`** explicitamente porque `tools.py` usa `apt-get install --no-install-recommends`: sem isso, o metapacote `weechat` pode satisfazer-se **só** com `weechat-headless` e o comando `chat` deixa de encontrar cliente interactivo (`weechat` / `weechat-curses` no PATH). 2. Cópia de `tools/bin/` para `/usr/local/bin` (`runv-help`, `runv-links`, `runv-status`, `chat`, `runv-profile`, `runv-finger`, `runv-who`, `runv-bulletin`, …) e de `tools/lib/runv_community.py` para `/usr/local/share/runv/lib/`. -3. MOTD dinâmico: `tools/motd/60-runv` → `/etc/update-motd.d/60-runv`. +3. MOTD dinâmico: `tools/motd/60-runv` → `/etc/update-motd.d/60-runv` (ver secção [MOTD](#motd-e-runv-help) abaixo). 4. Modelos para novas contas: `tools/skel/` → `/etc/skel/` (inclui `.plan`, `.project`, `.runv/profile.json`). 5. Drop-in SSH para utilizadores jailed: `tools/sshd/90-runv-jailed.conf` → `/etc/ssh/sshd_config.d/`. 6. Sudo administrativo para `pmurad-admin`: `tools/sudoers/90-runv-pmurad-admin` → `/etc/sudoers.d/`. @@ -40,4 +40,47 @@ Flags úteis: `--force`, `--skip-apt`, `--reconcile-existing-users` (ver `--help - Membros normais continuam a usar o modelo `runv-jailed` + `ChrootDirectory /srv/jail/%u`, para não saírem das respetivas homes na shell SSH normal. - `tools/tools.py` não altera contas já existentes por omissão. Se quiser reconciliar jail SSH e IRC em membros antigos, use `--reconcile-existing-users`. +## MOTD e `runv-help` + +O ficheiro [`tools/motd/60-runv`](../tools/motd/60-runv) gera a mensagem de boas-vindas no login SSH (via `update-motd.d`). Secções: + +| Secção | Conteúdo | +|--------|----------| +| Arte RUNV + tagline | Identidade visual alinhada ao site | +| Comandos úteis | `runv-help`, `runv-links`, `lynx`, `tmux`, `byobu`, `mutt`, `chat`, `runvers`, `runv-games` | +| Comunidade runv | `runv-profile`, `runv-finger`, `runv-who`, `runv-bulletin`, `runv-email-alias` | +| Últimas sessões SSH | Grelha 3×3 com as **9 sessões mais recentes** em `wtmp` (`last -w`); pode repetir o mesmo utilizador; **não** é quem está online agora | + +A ajuda completa está em `runv-help` (inclui a secção Comunidade e email de membro). Detalhes dos comandos: [17-community-commands.md](17-community-commands.md) e [08-email.md](08-email.md). + +### Cache do MOTD (Debian) + +O conteúdo mostrado no login costuma vir de `/run/motd.dynamic`, gerado por `run-parts /etc/update-motd.d/`. Em alguns sistemas o ficheiro é **actualizado em intervalo** (`motd-news`), não em cada login — a grelha de sessões pode parecer «congelada» até o cache refrescar. + +**Diagnóstico (admin):** + +```bash +stat /run/motd.dynamic +sudo /etc/update-motd.d/60-runv | tail -25 +``` + +**Forçar refresh após alterar o script:** + +```bash +cd /opt/runv-server/tools +sudo python3 tools.py --skip-apt --force +sudo run-parts /etc/update-motd.d > /run/motd.dynamic +``` + +Se o MOTD continuar desactualizado entre logins, rever `/etc/default/motd-news` na VPS (intervalo ou desactivar cache), conforme a política do servidor. + +### `runv-who` e `users.json` + +Para membros listarem utilizadores sem varrer `/home` (e sem erros em homes inacessíveis), o ficheiro canónico deve ser legível pelo grupo `runv-members`: + +```bash +sudo chown root:runv-members /var/lib/runv/users.json +sudo chmod 640 /var/lib/runv/users.json +``` + Próximo: [06-site-and-apache.md](06-site-and-apache.md). diff --git a/docs/17-community-commands.md b/docs/17-community-commands.md @@ -13,7 +13,9 @@ Estes comandos dão mais vida pubnix ao servidor runv.club: São instalados por [`tools/tools.py`](../tools/tools.py) em `/usr/local/bin` (junto com `runv-help`, `chat`, etc.). A biblioteca partilhada fica em `/usr/local/share/runv/lib/runv_community.py`. -Não expõem email, chave pública nem fingerprint de `/var/lib/runv/users.json`. +No login SSH, o MOTD ([`tools/motd/60-runv`](../tools/motd/60-runv)) e `runv-help` listam estes comandos na secção **Comunidade runv**. Ver também [05-tools-and-system-experience.md](05-tools-and-system-experience.md#motd-e-runv-help). + +Não expõem email, chave pública nem fingerprint de `/var/lib/runv/users.json` (excepto `runv-who`, que usa só a lista de usernames em `users.json` quando legível). ## `runv-profile` @@ -296,7 +298,6 @@ Possíveis evoluções (fora do âmbito actual): - `runv-admin bulletin hide/delete` (moderação); - feed público do mural no site; -- integração com MOTD; - integração com Garden / Gotchi; - backfill admin para membros existentes. diff --git a/tools/bin/runv-help b/tools/bin/runv-help @@ -23,6 +23,15 @@ printf ' %brunv-links%b Links do projeto, site e parceiros.\n' "${G}" "${R} printf ' %brunvers%b Espaços e serviços da comunidade (square, plantit).\n' "${G}" "${R}" printf '\n' +printf '%b%bComunidade runv%b\n' "${Y}" "${B}" "${R}" +printf ' %brunv-profile%b Perfil local: runv-profile init | show | path\n' "${G}" "${R}" +printf ' %brunv-finger%b Ver perfil público de outro membro (ex.: runv-finger pablo).\n' "${G}" "${R}" +printf ' %brunv-who%b Listar membros (--active, --json, --limit N).\n' "${G}" "${R}" +printf ' %brunv-bulletin%b Mural no terminal: list | post (mensagens curtas).\n' "${G}" "${R}" +printf ' %brunv-email-alias%b Alias user@runv.club: request | status | cancel\n' "${G}" "${R}" +printf ' (só membros no grupo runv-members; aprovação pelos admins).\n' +printf '\n' + printf '%b%bFerramentas instaladas no servidor%b (exemplos)\n' "${Y}" "${B}" "${R}" printf ' %blynx%b Navegador web no terminal.\n' "${G}" "${R}" printf ' %bcurl%b / %bwget%b Transferir ficheiros e páginas pela linha de comando.\n' "${G}" "${R}" "${G}" "${R}" @@ -57,6 +66,7 @@ printf '\n' printf '%b%bDicas para começar%b\n' "${Y}" "${B}" "${R}" printf ' • Edite o site em %b~/public_html/index.html%b para começar.\n' "${C}" "${R}" +printf ' • Corra %brunv-profile init%b e preencha %b~/.plan%b / %b~/.project%b.\n' "${G}" "${R}" "${C}" "${R}" "${C}" "${R}" printf '\n' printf '%b%bAjuda e mais informações%b\n' "${Y}" "${B}" "${R}" diff --git a/tools/bin/runv-who b/tools/bin/runv-who @@ -32,8 +32,14 @@ import runv_community as rc # noqa: E402 def collect_member(username: str) -> dict[str, Any]: paths = rc.home_paths(username) - has_homepage = paths["public_index"].is_file() - homepage_mtime = rc.homepage_mtime_iso(paths["public_index"]) if has_homepage else None + try: + has_homepage = rc.path_is_file(paths["public_index"]) + homepage_mtime = ( + rc.homepage_mtime_iso(paths["public_index"]) if has_homepage else None + ) + except OSError: + has_homepage = False + homepage_mtime = None return { "username": username, "homepage": f"/~{username}/", diff --git a/tools/lib/runv_community.py b/tools/lib/runv_community.py @@ -6,6 +6,7 @@ Utilitários partilhados pelos comandos comunitários runv.club (stdlib apenas). from __future__ import annotations import json +import os import re import sys from datetime import datetime, timezone @@ -60,8 +61,15 @@ def validate_username(username: str) -> str: return u +def path_is_file(path: Path) -> bool: + try: + return path.is_file() + except OSError: + return False + + def read_text_limited(path: Path, *, max_bytes: int = MAX_READ_BYTES) -> str | None: - if not path.is_file(): + if not path_is_file(path): return None try: with path.open("rb") as f: @@ -195,9 +203,19 @@ def load_member_usernames( ) names_home: list[str] = [] if home_root.is_dir(): - for entry in home_root.iterdir(): - if entry.is_dir() and USERNAME_RE.fullmatch(entry.name): - names_home.append(entry.name) + try: + entries = home_root.iterdir() + except OSError: + entries = () + for entry in entries: + if not entry.is_dir() or not USERNAME_RE.fullmatch(entry.name): + continue + try: + if not os.access(entry, os.R_OK | os.X_OK): + continue + except OSError: + continue + names_home.append(entry.name) return sorted(set(names_home), key=str.lower), warning diff --git a/tools/motd/60-runv b/tools/motd/60-runv @@ -3,7 +3,7 @@ # # RUNV em verde (ANSI); tagline e rodapé — alinhado a site/public/index.html # e terminal/entre_app.py (RUNV_ASCII_ART / ASCII_TAGLINE). -# Estatísticas gerais: runv-status (só pmurad-admin); aqui: lista de comandos + acessos recentes. +# Estatísticas gerais: runv-status (só pmurad-admin); aqui: lista de comandos + sessões recentes. # # Cores: printf %b com literais \033 (POSIX). Sem echo -e. @@ -27,9 +27,9 @@ print_runv_art() { RUNV_ART } -# MOTD: "Últimos acessos recentes" — até 9 nomes únicos (last(1)) → grelha 3×3 -# Isto é histórico de sessões, não presença em tempo real; runv-status usa who(1). -# Cada nome aparece só uma vez: ordem = mais recente primeiro (primeira ocorrência em last). +# MOTD: últimas 9 sessões SSH (last(1)) → grelha 3×3 +# Histórico wtmp; não é presença em tempo real (runv-status + who para admin). +# Pode repetir o mesmo utilizador; ordem = sessões mais recentes primeiro. # update-motd.d costuma ter PATH mínimo: tentar /usr/bin/last e /bin/last. print_last_sessions_3x3() { LAST_CMD=$(command -v last 2>/dev/null) || LAST_CMD= @@ -41,18 +41,16 @@ print_last_sessions_3x3() { fi tf=$(mktemp -t runvmotd.XXXXXX 2>/dev/null) || tf=/tmp/runvmotd.$$ trap 'rm -f "$tf"' EXIT HUP INT - # Ignora reboot, wtmp, entre/root (conta de pedido / admin); até 9 nomes distintos - "$LAST_CMD" -n 500 2>/dev/null | awk ' + # Ignora reboot, wtmp, entre/root, contas *-admin; até 9 sessões (sem deduplicar user) + "$LAST_CMD" -w -n 80 2>/dev/null | awk ' /^reboot/ || /^wtmp/ || /^$/ { next } NF < 1 { next } { u = $1 if (u == "entre" || u == "root") next - if (!(u in seen)) { - seen[u] = 1 - print u - if (++n >= 9) exit - } + if (u ~ /-admin$/) next + print u + if (++n >= 9) exit } ' > "$tf" || true @@ -63,13 +61,14 @@ print_last_sessions_3x3() { return fi - printf '\n%bÚltimos acessos recentes%b\n' "${B}" "${R}" + printf '\n%bÚltimas sessões SSH%b\n' "${B}" "${R}" + printf ' %b(histórico wtmp; não é quem está online agora)%b\n' "${D}" "${R}" row=1 while [ "$row" -le 3 ]; do read -r c1 || c1='' read -r c2 || c2='' read -r c3 || c3='' - printf ' %b%-12s%b %b%-12s%b %b%-12s%b\n' \ + printf ' %b%-16s%b %b%-16s%b %b%-16s%b\n' \ "${Y}" "$c1" "${R}" \ "${Y}" "$c2" "${R}" \ "${Y}" "$c3" "${R}" @@ -83,16 +82,23 @@ print_runv_art printf '\n%s\n' '.club — um computador para compartilhar' printf '\n%bComandos úteis%b\n' "${B}" "${R}" -# Coluna do nome do comando alinhada (maior: runv-links = 10 chars) -printf ' %b%-12s%b %b—%b ajuda e boas práticas do runv.club.\n' "${G}" "runv-help" "${R}" "${D}" "${R}" -printf ' %b%-12s%b %b—%b links do site e do mantenedor.\n' "${G}" "runv-links" "${R}" "${D}" "${R}" -printf ' %b%-12s%b %b—%b navegador web no terminal.\n' "${G}" "lynx" "${R}" "${D}" "${R}" -printf ' %b%-12s%b %b—%b multiplexador de terminal (várias sessões).\n' "${G}" "tmux" "${R}" "${D}" "${R}" -printf ' %b%-12s%b %b—%b barra de estado e atalhos sobre tmux/screen.\n' "${G}" "byobu" "${R}" "${D}" "${R}" -printf ' %b%-12s%b %b—%b cliente de e-mail no terminal.\n' "${G}" "mutt" "${R}" "${D}" "${R}" -printf ' %b%-12s%b %b—%b IRC da rede da casa.\n' "${G}" "chat" "${R}" "${D}" "${R}" -printf ' %b%-12s%b %b—%b catálogo dos espaços e serviços da casa.\n' "${G}" "runvers" "${R}" "${D}" "${R}" -printf ' %b%-12s%b %b—%b catálogo de jogos do servidor.\n' "${G}" "runv-games" "${R}" "${D}" "${R}" +# Coluna do nome do comando alinhada (maior: runv-email-alias = 16 chars) +printf ' %b%-16s%b %b—%b ajuda e boas práticas do runv.club.\n' "${G}" "runv-help" "${R}" "${D}" "${R}" +printf ' %b%-16s%b %b—%b links do site e do mantenedor.\n' "${G}" "runv-links" "${R}" "${D}" "${R}" +printf ' %b%-16s%b %b—%b navegador web no terminal.\n' "${G}" "lynx" "${R}" "${D}" "${R}" +printf ' %b%-16s%b %b—%b multiplexador de terminal (várias sessões).\n' "${G}" "tmux" "${R}" "${D}" "${R}" +printf ' %b%-16s%b %b—%b barra de estado e atalhos sobre tmux/screen.\n' "${G}" "byobu" "${R}" "${D}" "${R}" +printf ' %b%-16s%b %b—%b cliente de e-mail no terminal.\n' "${G}" "mutt" "${R}" "${D}" "${R}" +printf ' %b%-16s%b %b—%b IRC da rede da casa.\n' "${G}" "chat" "${R}" "${D}" "${R}" +printf ' %b%-16s%b %b—%b catálogo dos espaços e serviços da casa.\n' "${G}" "runvers" "${R}" "${D}" "${R}" +printf ' %b%-16s%b %b—%b catálogo de jogos do servidor.\n' "${G}" "runv-games" "${R}" "${D}" "${R}" + +printf '\n%bComunidade runv%b\n' "${B}" "${R}" +printf ' %b%-16s%b %b—%b perfil local (.plan, .project).\n' "${G}" "runv-profile" "${R}" "${D}" "${R}" +printf ' %b%-16s%b %b—%b ver perfil de outro membro.\n' "${G}" "runv-finger" "${R}" "${D}" "${R}" +printf ' %b%-16s%b %b—%b listar membros da casa.\n' "${G}" "runv-who" "${R}" "${D}" "${R}" +printf ' %b%-16s%b %b—%b mural comunitário no terminal.\n' "${G}" "runv-bulletin" "${R}" "${D}" "${R}" +printf ' %b%-16s%b %b—%b pedir ou ver alias user@runv.club.\n' "${G}" "runv-email-alias" "${R}" "${D}" "${R}" print_last_sessions_3x3