package server import "sync" // ChatManager tracks real-time chat sessions between nodes. // // Classic BBSes like TAG used split-screen chat over serial lines. // Our approach is simpler: line-by-line messaging through Go channels. // A background goroutine on the receiving side injects incoming // messages into the user's terminal between their own input lines. // // Flow: // 1. User A pages user B (one-shot notification via SendToNode) // 2. User A enters chat mode → gets a channel via EnterChat // 3. User B enters chat mode targeting A → LinkChat connects them // 4. Messages typed by A are sent via SendChat → delivered to B's channel // 5. Either user types /quit → EndChat cleans up their side // // Thread-safe: called from multiple session goroutines concurrently. type ChatManager struct { mu sync.Mutex chans map[int]chan string // per-node incoming message channel links map[int]int // node -> partner node } // NewChatManager creates an empty chat manager. func NewChatManager() *ChatManager { return &ChatManager{ chans: make(map[int]chan string), links: make(map[int]int), } } // EnterChat registers a node for chat and returns its incoming // message channel. If the node already has a channel, returns it. func (cm *ChatManager) EnterChat(node int) <-chan string { cm.mu.Lock() defer cm.mu.Unlock() if ch, ok := cm.chans[node]; ok { return ch } ch := make(chan string, 16) // Buffered to avoid blocking sender cm.chans[node] = ch return ch } // LinkChat establishes a bidirectional chat link between two nodes. // Both nodes must have entered chat mode first (have channels). // Returns false if either node hasn't entered chat. func (cm *ChatManager) LinkChat(nodeA, nodeB int) bool { cm.mu.Lock() defer cm.mu.Unlock() _, aOK := cm.chans[nodeA] _, bOK := cm.chans[nodeB] if !aOK || !bOK { return false } cm.links[nodeA] = nodeB cm.links[nodeB] = nodeA return true } // SendChat delivers a message to the given node's chat partner. // Returns false if the node has no partner linked. func (cm *ChatManager) SendChat(fromNode int, msg string) bool { cm.mu.Lock() partnerNode, linked := cm.links[fromNode] var partnerCh chan string if linked { partnerCh = cm.chans[partnerNode] } cm.mu.Unlock() if !linked || partnerCh == nil { return false } // Non-blocking send — drop message if partner's buffer is full select { case partnerCh <- msg: return true default: return false } } // EndChat removes a node from chat and cleans up its link. // If the node had a partner, sends a disconnect notification // to the partner's channel and unlinks them. func (cm *ChatManager) EndChat(node int) { cm.mu.Lock() partner, linked := cm.links[node] if linked { delete(cm.links, node) delete(cm.links, partner) // Notify partner if ch, ok := cm.chans[partner]; ok { select { case ch <- "": // Empty string signals disconnect default: } } } if ch, ok := cm.chans[node]; ok { close(ch) delete(cm.chans, node) } cm.mu.Unlock() } // Partner returns the chat partner for a node, or 0 if not linked. func (cm *ChatManager) Partner(node int) int { cm.mu.Lock() defer cm.mu.Unlock() return cm.links[node] } // IsInChat returns true if the node has an active chat channel. func (cm *ChatManager) IsInChat(node int) bool { cm.mu.Lock() defer cm.mu.Unlock() _, ok := cm.chans[node] return ok }