Merge feat/irc-server: add ssh irc@ built-in IRC client

Brings the in-process IRC client (internal/irc) and the `irc@` SSH route on
top of the already-merged members-only Ergo server, completing the IRC
feature. The other commits on the branch (Ergo server, Tor routes) were
already merged via feat/qrypt-invite-issuer, so this applies only the client.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-06-14 11:47:34 +00:00
commit 52b06d8cfe
6 changed files with 592 additions and 8 deletions

View file

@ -56,6 +56,7 @@ import (
"github.com/profullstack/agentbbs/internal/forwardemail"
"github.com/profullstack/agentbbs/internal/games"
"github.com/profullstack/agentbbs/internal/hub"
"github.com/profullstack/agentbbs/internal/irc"
"github.com/profullstack/agentbbs/internal/mail"
"github.com/profullstack/agentbbs/internal/payments"
"github.com/profullstack/agentbbs/internal/plugin"
@ -265,6 +266,8 @@ func (a *app) router() wish.Middleware {
a.handleTorIRC(s)
case auth.IsTorName(user):
a.handleTorCmd(s)
case auth.IsIRCName(user):
a.handleIRC(s)
case isVideo:
a.handleVideo(s, code)
case user == "agent":
@ -938,6 +941,48 @@ func (a *app) handleTorIRC(s ssh.Session) {
}
}
// handleIRC drops a member into the BBS's own (members-only) IRC network using
// an in-process client: it authenticates to Ergo over SASL as the member and
// runs a Bubble Tea TUI. Free for any registered member; needs a PTY. Distinct
// from tor-irc@ (a client for remote servers over Tor).
func (a *app) handleIRC(s ssh.Session) {
fp := auth.Fingerprint(s.PublicKey())
if fp == "" {
wish.Println(s, "irc@ needs your registered SSH key. New here? ssh join@"+a.host)
_ = s.Exit(1)
return
}
u, found, err := a.st.UserByFingerprint(fp)
if err != nil || !found {
wish.Println(s, "the IRC network is members-only — register first: ssh join@"+a.host)
_ = s.Exit(1)
return
}
if u.Banned {
wish.Println(s, "this account is suspended.")
_ = s.Exit(1)
return
}
sessID, _ := a.st.RecordSession(u.ID, s.User(), remoteIP(s), "irc")
defer func() { _ = a.st.EndSession(sessID) }()
addr := strings.TrimSpace(os.Getenv("AGENTBBS_IRC_ADDR"))
if addr == "" {
addr = irc.DefaultAddr
}
log.Info("irc connect", "user", u.Name, "addr", addr)
c, err := irc.Dial(s.Context(), addr, u.Name)
if err != nil {
wish.Println(s, "irc: "+err.Error())
_ = s.Exit(1)
return
}
_ = c.Join(irc.DefaultChannel)
if err := irc.Run(s, c); err != nil {
wish.Println(s, "irc: "+err.Error())
}
}
// handleTorCmd runs an arbitrary command through Tor (torsocks) inside the
// member's pod, never on the host. Premium; requires a PTY.
func (a *app) handleTorCmd(s ssh.Session) {