132 lines
3.3 KiB
Go
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
|
|
}
|