package ircserver import ( "context" "encoding/json" "strings" "time" "git.eeqj.de/sneak/neoirc/internal/db" "git.eeqj.de/sneak/neoirc/pkg/irc" ) // relayMessages polls the client output queue and delivers // IRC-formatted messages to the TCP connection. It runs // in a goroutine for the lifetime of the connection. func (c *Conn) relayMessages(ctx context.Context) { // Use a ticker as a fallback; primary wakeup is via // broker notification. ticker := time.NewTicker(pollInterval) defer ticker.Stop() for { select { case <-ctx.Done(): return default: } // Drain any available messages. delivered := c.drainQueue(ctx) if delivered { // Tight loop while there are messages. continue } // Wait for notification or timeout. waitCh := c.brk.Wait(c.sessionID) select { case <-waitCh: // New message notification — loop back. case <-ticker.C: // Periodic check. case <-ctx.Done(): c.brk.Remove(c.sessionID, waitCh) return } } } const relayPollLimit = 100 // drainQueue polls the output queue and delivers all // pending messages. Returns true if at least one message // was delivered. func (c *Conn) drainQueue(ctx context.Context) bool { msgs, lastID, err := c.database.PollMessages( ctx, c.clientID, c.lastQueueID, relayPollLimit, ) if err != nil { return false } if len(msgs) == 0 { return false } for i := range msgs { c.deliverIRCMessage(ctx, &msgs[i]) } if lastID > c.lastQueueID { c.lastQueueID = lastID } return true } // deliverIRCMessage converts a db.IRCMessage to wire // protocol and sends it. // //nolint:cyclop // dispatch table func (c *Conn) deliverIRCMessage( _ context.Context, msg *db.IRCMessage, ) { command := msg.Command // Decode body as []string for the trailing text. var bodyLines []string if msg.Body != nil { _ = json.Unmarshal(msg.Body, &bodyLines) } text := "" if len(bodyLines) > 0 { text = bodyLines[0] } // Route by command type. switch { case isNumeric(command): c.deliverNumeric(msg, text) case command == irc.CmdPrivmsg || command == irc.CmdNotice: c.deliverTextMessage(msg, command, text) case command == irc.CmdJoin: c.deliverJoin(msg) case command == irc.CmdPart: c.deliverPart(msg, text) case command == irc.CmdNick: c.deliverNickChange(msg, text) case command == irc.CmdQuit: c.deliverQuitMsg(msg, text) case command == irc.CmdTopic: c.deliverTopicChange(msg, text) case command == irc.CmdKick: c.deliverKickMsg(msg, text) case command == "INVITE": c.deliverInviteMsg(msg, text) case command == irc.CmdMode: c.deliverMode(msg, text) case command == irc.CmdPing: // Server-originated PING — reply with PONG. c.sendFromServer("PING", c.serverSfx) default: // Unknown command — deliver as server notice. if text != "" { c.sendFromServer("NOTICE", c.nick, text) } } } // isNumeric returns true if the command is a 3-digit // numeric code. func isNumeric(cmd string) bool { return len(cmd) == 3 && cmd[0] >= '0' && cmd[0] <= '9' && cmd[1] >= '0' && cmd[1] <= '9' && cmd[2] >= '0' && cmd[2] <= '9' } // deliverNumeric sends a numeric reply. func (c *Conn) deliverNumeric( msg *db.IRCMessage, text string, ) { from := msg.From if from == "" { from = c.serverSfx } var params []string if msg.Params != nil { _ = json.Unmarshal(msg.Params, ¶ms) } allParams := make([]string, 0, 1+len(params)+1) allParams = append(allParams, c.nick) allParams = append(allParams, params...) if text != "" { allParams = append(allParams, text) } c.send(FormatMessage(from, msg.Command, allParams...)) } // deliverTextMessage sends PRIVMSG or NOTICE. func (c *Conn) deliverTextMessage( msg *db.IRCMessage, command, text string, ) { from := msg.From target := msg.To // Don't echo our own messages back. if strings.EqualFold(from, c.nick) { return } prefix := from if !strings.Contains(prefix, "!") { prefix = from + "!" + from + "@*" } c.send(FormatMessage(prefix, command, target, text)) } // deliverJoin sends a JOIN notification. func (c *Conn) deliverJoin(msg *db.IRCMessage) { // Don't echo our own JOINs (we already sent them // during joinChannel). if strings.EqualFold(msg.From, c.nick) { return } prefix := msg.From + "!" + msg.From + "@*" channel := msg.To c.send(FormatMessage(prefix, "JOIN", channel)) } // deliverPart sends a PART notification. func (c *Conn) deliverPart(msg *db.IRCMessage, text string) { if strings.EqualFold(msg.From, c.nick) { return } prefix := msg.From + "!" + msg.From + "@*" channel := msg.To if text != "" { c.send(FormatMessage( prefix, "PART", channel, text, )) } else { c.send(FormatMessage(prefix, "PART", channel)) } } // deliverNickChange sends a NICK change notification. func (c *Conn) deliverNickChange( msg *db.IRCMessage, newNick string, ) { if strings.EqualFold(msg.From, c.nick) { return } prefix := msg.From + "!" + msg.From + "@*" c.send(FormatMessage(prefix, "NICK", newNick)) } // deliverQuitMsg sends a QUIT notification. func (c *Conn) deliverQuitMsg( msg *db.IRCMessage, text string, ) { if strings.EqualFold(msg.From, c.nick) { return } prefix := msg.From + "!" + msg.From + "@*" if text != "" { c.send(FormatMessage( prefix, "QUIT", "Quit: "+text, )) } else { c.send(FormatMessage(prefix, "QUIT", "Quit")) } } // deliverTopicChange sends a TOPIC change notification. func (c *Conn) deliverTopicChange( msg *db.IRCMessage, text string, ) { prefix := msg.From + "!" + msg.From + "@*" channel := msg.To c.send(FormatMessage(prefix, "TOPIC", channel, text)) } // deliverKickMsg sends a KICK notification. func (c *Conn) deliverKickMsg( msg *db.IRCMessage, text string, ) { prefix := msg.From + "!" + msg.From + "@*" channel := msg.To var params []string if msg.Params != nil { _ = json.Unmarshal(msg.Params, ¶ms) } kickTarget := "" if len(params) > 0 { kickTarget = params[0] } if kickTarget != "" { c.send(FormatMessage( prefix, "KICK", channel, kickTarget, text, )) } else { c.send(FormatMessage( prefix, "KICK", channel, "?", text, )) } } // deliverInviteMsg sends an INVITE notification. func (c *Conn) deliverInviteMsg( _ *db.IRCMessage, text string, ) { c.sendFromServer("NOTICE", c.nick, text) } // deliverMode sends a MODE change notification. func (c *Conn) deliverMode( msg *db.IRCMessage, text string, ) { prefix := msg.From + "!" + msg.From + "@*" target := msg.To if text != "" { c.send(FormatMessage(prefix, "MODE", target, text)) } }