mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 14:27:27 +00:00
Wire shared MOTD into BBS hub and IRC
Use profullstack.com/motd as the shared Message of the Day across both the SSH BBS hub and the Ergo IRC server. - internal/motd: fetch + in-memory cache with background refresh (stdlib only); Current() never blocks session start, keeps last value on error. Source overridable via AGENTBBS_MOTD_URL. - hub: append the daily MOTD below the existing welcome/onboarding text. - IRC: deploy/ergo/refresh-motd.sh pulls /motd into Ergo's ergo.motd and rehashes; setup.sh installs it + an ergo-motd.timer (hourly) mirroring the ergo-certs timer, with a seeded fallback if the source is offline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
63548bcbbe
commit
fb7722eacc
4 changed files with 177 additions and 6 deletions
|
|
@ -68,6 +68,7 @@ import (
|
||||||
"github.com/profullstack/agentbbs/internal/mail"
|
"github.com/profullstack/agentbbs/internal/mail"
|
||||||
"github.com/profullstack/agentbbs/internal/mailbox"
|
"github.com/profullstack/agentbbs/internal/mailbox"
|
||||||
"github.com/profullstack/agentbbs/internal/mailu"
|
"github.com/profullstack/agentbbs/internal/mailu"
|
||||||
|
"github.com/profullstack/agentbbs/internal/motd"
|
||||||
"github.com/profullstack/agentbbs/internal/news"
|
"github.com/profullstack/agentbbs/internal/news"
|
||||||
"github.com/profullstack/agentbbs/internal/payments"
|
"github.com/profullstack/agentbbs/internal/payments"
|
||||||
"github.com/profullstack/agentbbs/internal/plugin"
|
"github.com/profullstack/agentbbs/internal/plugin"
|
||||||
|
|
@ -238,6 +239,12 @@ func main() {
|
||||||
}
|
}
|
||||||
log.Info("sandbox", "mode", a.sandbox.Mode())
|
log.Info("sandbox", "mode", a.sandbox.Mode())
|
||||||
|
|
||||||
|
// Shared Message of the Day from profullstack.com — shown on the hub (and on
|
||||||
|
// IRC via Ergo's MOTD). Cached + refreshed in the background so it never
|
||||||
|
// blocks session start. Override the source with AGENTBBS_MOTD_URL (empty
|
||||||
|
// disables remote fetch).
|
||||||
|
motd.Start(context.Background(), env("AGENTBBS_MOTD_URL", motd.DefaultURL), 30*time.Minute)
|
||||||
|
|
||||||
// Email confirmation endpoint (the link in the join@ verification mail).
|
// Email confirmation endpoint (the link in the join@ verification mail).
|
||||||
// Loopback only; Caddy reverse-proxies /verify to it. Separate from the
|
// Loopback only; Caddy reverse-proxies /verify to it. Separate from the
|
||||||
// on-demand-TLS ask server above.
|
// on-demand-TLS ask server above.
|
||||||
|
|
@ -396,15 +403,22 @@ var bannerStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#e11
|
||||||
func (a *app) hubMOTD(u auth.User) string {
|
func (a *app) hubMOTD(u auth.User) string {
|
||||||
body := env("AGENTBBS_MOTD",
|
body := env("AGENTBBS_MOTD",
|
||||||
"A terminal BBS for humans & AI agents.\nGames · IRC · News · a Linux pod · your own homepage.")
|
"A terminal BBS for humans & AI agents.\nGames · IRC · News · a Linux pod · your own homepage.")
|
||||||
|
var s string
|
||||||
if u.Kind == auth.Guest {
|
if u.Kind == auth.Guest {
|
||||||
return "You're browsing as a guest.\n" + body +
|
s = "You're browsing as a guest.\n" + body +
|
||||||
"\nssh join@" + a.host + " to claim a username, a pod & a homepage."
|
"\nssh join@" + a.host + " to claim a username, a pod & a homepage."
|
||||||
|
} else {
|
||||||
|
welcome := "Welcome back, " + u.Name + "."
|
||||||
|
if n, err := a.st.UnreadCount(u.Name); err == nil && n > 0 {
|
||||||
|
welcome += fmt.Sprintf(" 📬 %d unread — open Members ▸ inbox (i).", n)
|
||||||
|
}
|
||||||
|
s = welcome + "\n" + body
|
||||||
}
|
}
|
||||||
welcome := "Welcome back, " + u.Name + "."
|
// Append the shared daily Message of the Day from profullstack.com, if loaded.
|
||||||
if n, err := a.st.UnreadCount(u.Name); err == nil && n > 0 {
|
if m := motd.Current(); m != "" {
|
||||||
welcome += fmt.Sprintf(" 📬 %d unread — open Members ▸ inbox (i).", n)
|
s += "\n\n" + m
|
||||||
}
|
}
|
||||||
return welcome + "\n" + body
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// teaHandler builds the hub model for guests, members, and agents.
|
// teaHandler builds the hub model for guests, members, and agents.
|
||||||
|
|
|
||||||
37
deploy/ergo/refresh-motd.sh
Executable file
37
deploy/ergo/refresh-motd.sh
Executable file
|
|
@ -0,0 +1,37 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# refresh-motd.sh — pull the shared Message of the Day from profullstack.com
|
||||||
|
# into Ergo's MOTD file and reload Ergo if it changed. setup.sh installs this to
|
||||||
|
# /usr/local/bin/ergo-refresh-motd and runs it from the ergo-motd.timer, so the
|
||||||
|
# IRC server's MOTD (shown on connect / via /MOTD) tracks profullstack.com/motd.
|
||||||
|
#
|
||||||
|
# Ergo resolves the `motd:` path in ircd.yaml relative to the config dir, so the
|
||||||
|
# file lives at $ERGO_CONF_DIR/ergo.motd (default /etc/ergo/ergo.motd).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOTD_URL="${MOTD_URL:-https://profullstack.com/motd}"
|
||||||
|
ERGO_CONF_DIR="${ERGO_CONF_DIR:-/etc/ergo}"
|
||||||
|
DST="$ERGO_CONF_DIR/ergo.motd"
|
||||||
|
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
trap 'rm -f "$tmp"' EXIT
|
||||||
|
|
||||||
|
if curl -fsSL --max-time 10 "$MOTD_URL" -o "$tmp" && [ -s "$tmp" ]; then
|
||||||
|
if ! cmp -s "$tmp" "$DST"; then
|
||||||
|
install -m 0644 "$tmp" "$DST"
|
||||||
|
chown ergo:ergo "$DST" 2>/dev/null || true
|
||||||
|
echo "updated Ergo MOTD from $MOTD_URL"
|
||||||
|
# Ergo rehashes config (incl. MOTD) on SIGHUP (systemctl reload).
|
||||||
|
systemctl reload ergo 2>/dev/null || systemctl restart ergo 2>/dev/null || true
|
||||||
|
else
|
||||||
|
echo "Ergo MOTD already current"
|
||||||
|
fi
|
||||||
|
elif [ ! -s "$DST" ]; then
|
||||||
|
# First run and the source is unreachable — seed a minimal MOTD so Ergo has
|
||||||
|
# something to serve; the timer replaces it once profullstack.com is reachable.
|
||||||
|
printf '%s\n' "Welcome to AgentBBS IRC." > "$DST"
|
||||||
|
chown ergo:ergo "$DST" 2>/dev/null || true
|
||||||
|
echo "MOTD source unreachable; seeded placeholder"
|
||||||
|
else
|
||||||
|
echo "MOTD source unreachable; keeping existing MOTD"
|
||||||
|
fi
|
||||||
90
internal/motd/motd.go
Normal file
90
internal/motd/motd.go
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
// Package motd fetches the shared Message of the Day from profullstack.com and
|
||||||
|
// caches it in memory, refreshing in the background. The hub MOTD reads the
|
||||||
|
// cached value (never blocking session start); a failed fetch keeps the last
|
||||||
|
// known value. Set AGENTBBS_MOTD_URL to override the source (empty disables).
|
||||||
|
package motd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultURL is the canonical shared MOTD endpoint (plain text, CORS-open).
|
||||||
|
const DefaultURL = "https://profullstack.com/motd"
|
||||||
|
|
||||||
|
var (
|
||||||
|
mu sync.RWMutex
|
||||||
|
cached string
|
||||||
|
)
|
||||||
|
|
||||||
|
// Current returns the most recently fetched MOTD, or "" if none has been
|
||||||
|
// fetched yet (or fetching is disabled/failing).
|
||||||
|
func Current() string {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
func set(s string) {
|
||||||
|
mu.Lock()
|
||||||
|
cached = s
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetch(ctx context.Context, url string) (string, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", &httpError{resp.StatusCode}
|
||||||
|
}
|
||||||
|
// Cap the read; the MOTD is a short text blob.
|
||||||
|
b, err := io.ReadAll(io.LimitReader(resp.Body, 8<<10))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(b)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpError struct{ code int }
|
||||||
|
|
||||||
|
func (e *httpError) Error() string { return "motd: unexpected status " + http.StatusText(e.code) }
|
||||||
|
|
||||||
|
// Start fetches the MOTD once (blocking, with a short timeout so a slow source
|
||||||
|
// can't stall boot for long) then refreshes every `every` until ctx is done.
|
||||||
|
// A url of "" disables fetching entirely.
|
||||||
|
func Start(ctx context.Context, url string, every time.Duration) {
|
||||||
|
if url == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refresh := func() {
|
||||||
|
c, cancel := context.WithTimeout(ctx, 8*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if s, err := fetch(c, url); err == nil && s != "" {
|
||||||
|
set(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refresh()
|
||||||
|
go func() {
|
||||||
|
t := time.NewTicker(every)
|
||||||
|
defer t.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
32
setup.sh
32
setup.sh
|
|
@ -859,6 +859,12 @@ if [ "$IRC" = "1" ]; then
|
||||||
-keyout "$ERGO_DATA/tls/privkey.pem" -out "$ERGO_DATA/tls/fullchain.pem" \
|
-keyout "$ERGO_DATA/tls/privkey.pem" -out "$ERGO_DATA/tls/fullchain.pem" \
|
||||||
-subj "/CN=${IRC_DOMAIN}" 2>/dev/null
|
-subj "/CN=${IRC_DOMAIN}" 2>/dev/null
|
||||||
fi
|
fi
|
||||||
|
# MOTD: pull the shared Message of the Day from profullstack.com into Ergo's
|
||||||
|
# MOTD file (shown on IRC connect / via /MOTD). The ergo-motd.timer keeps it
|
||||||
|
# fresh; Ergo reads it on start, so write it before the service comes up.
|
||||||
|
install -m 0755 "${SRC_DIR}/deploy/ergo/refresh-motd.sh" /usr/local/bin/ergo-refresh-motd
|
||||||
|
ERGO_CONF_DIR=/etc/ergo /usr/local/bin/ergo-refresh-motd || true
|
||||||
|
|
||||||
chown -R ergo:ergo "$ERGO_DATA" /etc/ergo
|
chown -R ergo:ergo "$ERGO_DATA" /etc/ergo
|
||||||
|
|
||||||
# Initialize the datastore once.
|
# Initialize the datastore once.
|
||||||
|
|
@ -910,6 +916,29 @@ OnBootSec=5min
|
||||||
OnUnitActiveSec=12h
|
OnUnitActiveSec=12h
|
||||||
Persistent=true
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
UNIT
|
||||||
|
|
||||||
|
# Hourly MOTD refresh from profullstack.com/motd (shared across all properties).
|
||||||
|
cat > /etc/systemd/system/ergo-motd.service <<UNIT
|
||||||
|
[Unit]
|
||||||
|
Description=Refresh Ergo MOTD from profullstack.com for ${IRC_DOMAIN}
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
Environment=ERGO_CONF_DIR=/etc/ergo
|
||||||
|
ExecStart=/usr/local/bin/ergo-refresh-motd
|
||||||
|
UNIT
|
||||||
|
cat > /etc/systemd/system/ergo-motd.timer <<UNIT
|
||||||
|
[Unit]
|
||||||
|
Description=Periodic Ergo MOTD refresh from profullstack.com
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=5min
|
||||||
|
OnUnitActiveSec=1h
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=timers.target
|
WantedBy=timers.target
|
||||||
UNIT
|
UNIT
|
||||||
|
|
@ -918,12 +947,13 @@ UNIT
|
||||||
systemctl enable ergo >/dev/null 2>&1 || true
|
systemctl enable ergo >/dev/null 2>&1 || true
|
||||||
systemctl restart ergo
|
systemctl restart ergo
|
||||||
systemctl enable --now ergo-certs.timer >/dev/null 2>&1 || true
|
systemctl enable --now ergo-certs.timer >/dev/null 2>&1 || true
|
||||||
|
systemctl enable --now ergo-motd.timer >/dev/null 2>&1 || true
|
||||||
ufw allow 6697/tcp >/dev/null
|
ufw allow 6697/tcp >/dev/null
|
||||||
sleep 1
|
sleep 1
|
||||||
systemctl is-active --quiet ergo \
|
systemctl is-active --quiet ergo \
|
||||||
|| warn "ergo failed to start — check: journalctl -u ergo -n50"
|
|| warn "ergo failed to start — check: journalctl -u ergo -n50"
|
||||||
else
|
else
|
||||||
systemctl disable --now ergo ergo-certs.timer >/dev/null 2>&1 || true
|
systemctl disable --now ergo ergo-certs.timer ergo-motd.timer >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---- 9c. News (Usenet/NNTP) server (co-located news.${DOMAIN}) --------------
|
# ---- 9c. News (Usenet/NNTP) server (co-located news.${DOMAIN}) --------------
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue