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