package main import ( "net" "os" "sync" "time" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/log" "github.com/charmbracelet/ssh" "github.com/charmbracelet/wish" "github.com/charmbracelet/wish/activeterm" bm "github.com/charmbracelet/wish/bubbletea" "github.com/charmbracelet/wish/logging" "github.com/muesli/termenv" "github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/bluesky-social/indigo/atproto/syntax" ) // --- user session: holds the atproto api client for posting --- type userSession struct { handle string did syntax.DID oauthApp *oauth.ClientApp sessData *oauth.ClientSessionData } // --- chat message --- type chatMsg struct { sender string text string system bool ts time.Time } // --- app --- type app struct { *ssh.Server mu sync.Mutex progs map[string]*tea.Program sessions map[string]*tea.Program userSessions map[string]*userSession // handle -> userSession users map[string]string // handle -> color colorIdx int history []chatMsg keys *keyStore } func newApp() *app { os.MkdirAll(keysDir, 0700) a := &app{ progs: make(map[string]*tea.Program), sessions: make(map[string]*tea.Program), userSessions: make(map[string]*userSession), users: make(map[string]string), keys: newKeyStore(keysDir + "/keys.json"), } s, err := wish.NewServer( wish.WithAddress(net.JoinHostPort(host, port)), wish.WithHostKeyPath(".ssh/id_ed25519"), wish.WithPublicKeyAuth(func(_ ssh.Context, _ ssh.PublicKey) bool { return true // accept all keys; we check them for identity }), wish.WithMiddleware( goodbyeMiddleware(), bm.MiddlewareWithProgramHandler(a.programHandler, termenv.ANSI256), pdsGateMiddleware(), activeterm.Middleware(), logging.Middleware(), ), ) if err != nil { log.Fatal("could not create server", "err", err) } a.Server = s return a } func (a *app) assignColor(handle string) string { a.mu.Lock() defer a.mu.Unlock() if c, ok := a.users[handle]; ok { return c } c := userColors[a.colorIdx%len(userColors)] a.colorIdx++ a.users[handle] = c return c } func (a *app) promoteSession(sessionID, handle string) { a.mu.Lock() defer a.mu.Unlock() if p, ok := a.sessions[sessionID]; ok { a.progs[handle] = p delete(a.sessions, sessionID) } } func (a *app) removeProg(handle string) { a.mu.Lock() defer a.mu.Unlock() delete(a.progs, handle) } func (a *app) removeSession(sessionID string) { a.mu.Lock() defer a.mu.Unlock() delete(a.sessions, sessionID) } func (a *app) getHistory() []chatMsg { a.mu.Lock() defer a.mu.Unlock() h := make([]chatMsg, len(a.history)) copy(h, a.history) return h } func (a *app) broadcast(msg chatMsg) { a.mu.Lock() a.history = append(a.history, msg) if len(a.history) > 500 { a.history = a.history[len(a.history)-500:] } progs := make([]*tea.Program, 0, len(a.progs)) for _, p := range a.progs { progs = append(progs, p) } a.mu.Unlock() for _, p := range progs { go p.Send(msg) } } func (a *app) onlineUsers() []string { a.mu.Lock() defer a.mu.Unlock() users := make([]string, 0, len(a.progs)) for h := range a.progs { users = append(users, h) } return users } func (a *app) setUserSession(handle string, us *userSession) { a.mu.Lock() defer a.mu.Unlock() a.userSessions[handle] = us } func (a *app) getUserSession(handle string) *userSession { a.mu.Lock() defer a.mu.Unlock() return a.userSessions[handle] } func (a *app) programHandler(s ssh.Session) *tea.Program { renderer := bm.MakeRenderer(s) sessionID := s.RemoteAddr().String() sshUser := s.User() pubkey := s.PublicKey() // check if we already know this key knownHandle := a.keys.lookup(pubkey) m := newModel(a, renderer, sessionID, sshUser, pubkey, knownHandle) p := tea.NewProgram(m, append(bm.MakeOptions(s), tea.WithAltScreen())...) a.mu.Lock() a.sessions[sessionID] = p a.mu.Unlock() return p }