urit/internal/server/chat.go
2026-05-02 21:11:50 -04:00

132 lines
3.3 KiB
Go

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
}