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/mailbox"
|
||||
"github.com/profullstack/agentbbs/internal/mailu"
|
||||
"github.com/profullstack/agentbbs/internal/motd"
|
||||
"github.com/profullstack/agentbbs/internal/news"
|
||||
"github.com/profullstack/agentbbs/internal/payments"
|
||||
"github.com/profullstack/agentbbs/internal/plugin"
|
||||
|
|
@ -238,6 +239,12 @@ func main() {
|
|||
}
|
||||
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).
|
||||
// Loopback only; Caddy reverse-proxies /verify to it. Separate from the
|
||||
// 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 {
|
||||
body := env("AGENTBBS_MOTD",
|
||||
"A terminal BBS for humans & AI agents.\nGames · IRC · News · a Linux pod · your own homepage.")
|
||||
var s string
|
||||
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."
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
return welcome + "\n" + body
|
||||
s = welcome + "\n" + body
|
||||
}
|
||||
// Append the shared daily Message of the Day from profullstack.com, if loaded.
|
||||
if m := motd.Current(); m != "" {
|
||||
s += "\n\n" + m
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// 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" \
|
||||
-subj "/CN=${IRC_DOMAIN}" 2>/dev/null
|
||||
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
|
||||
|
||||
# Initialize the datastore once.
|
||||
|
|
@ -910,6 +916,29 @@ OnBootSec=5min
|
|||
OnUnitActiveSec=12h
|
||||
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]
|
||||
WantedBy=timers.target
|
||||
UNIT
|
||||
|
|
@ -918,12 +947,13 @@ UNIT
|
|||
systemctl enable ergo >/dev/null 2>&1 || true
|
||||
systemctl restart ergo
|
||||
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
|
||||
sleep 1
|
||||
systemctl is-active --quiet ergo \
|
||||
|| warn "ergo failed to start — check: journalctl -u ergo -n50"
|
||||
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
|
||||
|
||||
# ---- 9c. News (Usenet/NNTP) server (co-located news.${DOMAIN}) --------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue