mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 14:27:27 +00:00
Video calls (PairUX→ASCII), agent@ chat, and finger routes
ssh video-<code>@ joins a PairUX/LiveKit call rendered as truecolor ASCII (▀ half-blocks, 2 pixels per cell); video@ prompts for a code. Codes are minted by PairUX only. Pipeline: VP8 RTP → PLI keyframe requests → ivfwriter remux → ffmpeg decode/scale → RGB24 → ANSI → bubbletea over the SSH PTY. Subscriber-only, no audio in v1. ssh agent@ opens a persisted chat with the operator's agent — AGENTBBS_AGENT_CMD runs per message (stdin→stdout), e.g. `claude -p`. ssh <member>@ with someone else's name prints a classic finger card (.plan, member since, last seen) and disconnects; your own name still enters the hub. cmd/lkpublish: dev publisher for testing (explicit -fps pacing; lksdk IVF replay mispaces from file timebase alone, measured 1fps from a 15fps file; dimensions required or dynacast pauses the track). Verified end-to-end against livekit-server --dev: 128k truecolor cells / 20k distinct colors streamed over a real SSH session; chat round-trip and finger card verified over SSH. Go toolchain pinned to 1.26 via mise.toml (lksdk requirement). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f3b085a08f
commit
9b0f465946
14 changed files with 1397 additions and 46 deletions
|
|
@ -35,6 +35,10 @@ type Store interface {
|
|||
EnsureUser(name, kind, pubkeyFP string) (User, error)
|
||||
// UserByFingerprint finds an account by SSH key fingerprint.
|
||||
UserByFingerprint(fp string) (User, bool, error)
|
||||
// UserByName finds an account by exact username (no creation).
|
||||
UserByName(name string) (User, bool, error)
|
||||
// LastSeen reports the start of the user's most recent session.
|
||||
LastSeen(userID int64) (time.Time, bool, error)
|
||||
|
||||
RecordSession(userID int64, username, remote, route string) (int64, error)
|
||||
EndSession(sessionID int64) error
|
||||
|
|
@ -46,9 +50,20 @@ type Store interface {
|
|||
PodPaidUntil(userID int64) (time.Time, bool, error)
|
||||
GrantPod(userID int64, until time.Time, paymentRef string) error
|
||||
|
||||
// Chat transcripts for the agent@ surface.
|
||||
AddChat(userID int64, username, role, text string) error
|
||||
RecentChats(username string, n int) ([]ChatMessage, error)
|
||||
|
||||
Close() error
|
||||
}
|
||||
|
||||
// ChatMessage is one line of an agent@ conversation.
|
||||
type ChatMessage struct {
|
||||
Role string // "user" or "agent"
|
||||
Text string
|
||||
At time.Time
|
||||
}
|
||||
|
||||
// ErrKeyMismatch means a username is already registered with another key.
|
||||
var ErrKeyMismatch = errors.New("username registered with a different key")
|
||||
|
||||
|
|
@ -92,6 +107,15 @@ CREATE TABLE IF NOT EXISTS scores (
|
|||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_scores_game ON scores(game, score DESC);
|
||||
CREATE TABLE IF NOT EXISTS chat_messages (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER,
|
||||
username TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_user ON chat_messages(username, id);
|
||||
CREATE TABLE IF NOT EXISTS pod_subscriptions (
|
||||
user_id INTEGER PRIMARY KEY REFERENCES users(id),
|
||||
paid_until TEXT NOT NULL,
|
||||
|
|
@ -211,4 +235,65 @@ func (s *sqliteStore) GrantPod(userID int64, until time.Time, ref string) error
|
|||
return err
|
||||
}
|
||||
|
||||
func (s *sqliteStore) UserByName(name string) (User, bool, error) {
|
||||
var u User
|
||||
var created string
|
||||
err := s.db.QueryRow(`SELECT id, name, kind, pubkey_fp, created_at FROM users WHERE name = ?`, name).
|
||||
Scan(&u.ID, &u.Name, &u.Kind, &u.PubKeyFP, &created)
|
||||
if err == sql.ErrNoRows {
|
||||
return User{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return User{}, false, err
|
||||
}
|
||||
u.CreatedAt, _ = time.Parse(time.RFC3339, created)
|
||||
return u, true, nil
|
||||
}
|
||||
|
||||
func (s *sqliteStore) LastSeen(userID int64) (time.Time, bool, error) {
|
||||
var at string
|
||||
err := s.db.QueryRow(`SELECT started_at FROM sessions WHERE user_id = ? ORDER BY id DESC LIMIT 1`, userID).Scan(&at)
|
||||
if err == sql.ErrNoRows {
|
||||
return time.Time{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return time.Time{}, false, err
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339, at)
|
||||
return t, err == nil, err
|
||||
}
|
||||
|
||||
func (s *sqliteStore) AddChat(userID int64, username, role, text string) error {
|
||||
var uid any
|
||||
if userID > 0 {
|
||||
uid = userID
|
||||
}
|
||||
_, err := s.db.Exec(`INSERT INTO chat_messages (user_id, username, role, text) VALUES (?,?,?,?)`,
|
||||
uid, username, role, text)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *sqliteStore) RecentChats(username string, n int) ([]ChatMessage, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT role, text, created_at FROM (
|
||||
SELECT id, role, text, created_at FROM chat_messages
|
||||
WHERE username = ? ORDER BY id DESC LIMIT ?
|
||||
) ORDER BY id ASC`, username, n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []ChatMessage
|
||||
for rows.Next() {
|
||||
var m ChatMessage
|
||||
var at string
|
||||
if err := rows.Scan(&m.Role, &m.Text, &at); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.At, _ = time.Parse(time.RFC3339, at)
|
||||
out = append(out, m)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *sqliteStore) Close() error { return s.db.Close() }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue