All checks were successful
check / check (push) Successful in 58s
Add a backward-compatible IRC protocol listener (RFC 1459/2812) that allows standard IRC clients (irssi, weechat, hexchat, etc.) to connect directly via TCP. Key features: - TCP listener on configurable port (IRC_LISTEN_ADDR env var, e.g. :6667) - Full IRC wire protocol parsing and formatting - Connection registration (NICK + USER + optional PASS) - Channel operations: JOIN, PART, MODE, TOPIC, NAMES, LIST, KICK, INVITE - Messaging: PRIVMSG, NOTICE (channel and direct) - Info commands: WHO, WHOIS, LUSERS, MOTD, AWAY - Operator support: OPER (with configured credentials) - PING/PONG keepalive - CAP negotiation (for modern client compatibility) - Full bridge to HTTP/JSON API (shared DB, broker, sessions) - Real-time message relay via broker notifications - Comprehensive test suite (parser + integration tests) The IRC listener is an optional component — disabled when IRC_LISTEN_ADDR is empty (the default). The Broker is now an Fx-provided dependency shared between HTTP handlers and the IRC server. closes #89
124 lines
2.5 KiB
Go
124 lines
2.5 KiB
Go
// Package ircserver implements a traditional IRC wire protocol
|
|
// listener (RFC 1459/2812) that bridges to the neoirc HTTP/JSON
|
|
// server internals.
|
|
package ircserver
|
|
|
|
import "strings"
|
|
|
|
// Message represents a parsed IRC wire protocol message.
|
|
type Message struct {
|
|
// Prefix is the optional :prefix at the start (may be
|
|
// empty for client-to-server messages).
|
|
Prefix string
|
|
// Command is the IRC command (e.g., "PRIVMSG", "NICK").
|
|
Command string
|
|
// Params holds the positional parameters, including the
|
|
// trailing parameter (which was preceded by ':' on the
|
|
// wire).
|
|
Params []string
|
|
}
|
|
|
|
// ParseMessage parses a single IRC wire protocol line
|
|
// (without the trailing CR-LF) into a Message.
|
|
// Returns nil if the line is empty.
|
|
//
|
|
// IRC message format (RFC 1459 §2.3.1):
|
|
//
|
|
// [":" prefix SPACE] command { SPACE param } [SPACE ":" trailing]
|
|
func ParseMessage(line string) *Message {
|
|
if line == "" {
|
|
return nil
|
|
}
|
|
|
|
msg := &Message{} //nolint:exhaustruct // fields set below
|
|
|
|
// Extract prefix if present.
|
|
if line[0] == ':' {
|
|
idx := strings.IndexByte(line, ' ')
|
|
if idx < 0 {
|
|
// Only a prefix, no command — invalid.
|
|
return nil
|
|
}
|
|
|
|
msg.Prefix = line[1:idx]
|
|
line = line[idx+1:]
|
|
}
|
|
|
|
// Skip leading spaces.
|
|
line = strings.TrimLeft(line, " ")
|
|
if line == "" {
|
|
return nil
|
|
}
|
|
|
|
// Extract command.
|
|
idx := strings.IndexByte(line, ' ')
|
|
if idx < 0 {
|
|
msg.Command = strings.ToUpper(line)
|
|
|
|
return msg
|
|
}
|
|
|
|
msg.Command = strings.ToUpper(line[:idx])
|
|
line = line[idx+1:]
|
|
|
|
// Extract parameters.
|
|
for line != "" {
|
|
line = strings.TrimLeft(line, " ")
|
|
if line == "" {
|
|
break
|
|
}
|
|
|
|
// Trailing parameter (everything after ':').
|
|
if line[0] == ':' {
|
|
msg.Params = append(msg.Params, line[1:])
|
|
|
|
break
|
|
}
|
|
|
|
idx = strings.IndexByte(line, ' ')
|
|
if idx < 0 {
|
|
msg.Params = append(msg.Params, line)
|
|
|
|
break
|
|
}
|
|
|
|
msg.Params = append(msg.Params, line[:idx])
|
|
line = line[idx+1:]
|
|
}
|
|
|
|
return msg
|
|
}
|
|
|
|
// FormatMessage formats an IRC message into wire protocol
|
|
// format (without the trailing CR-LF).
|
|
func FormatMessage(
|
|
prefix, command string,
|
|
params ...string,
|
|
) string {
|
|
var buf strings.Builder
|
|
|
|
if prefix != "" {
|
|
buf.WriteByte(':')
|
|
buf.WriteString(prefix)
|
|
buf.WriteByte(' ')
|
|
}
|
|
|
|
buf.WriteString(command)
|
|
|
|
for i, param := range params {
|
|
buf.WriteByte(' ')
|
|
|
|
isLast := i == len(params)-1
|
|
needsColon := strings.Contains(param, " ") ||
|
|
param == "" || param[0] == ':'
|
|
|
|
if isLast && needsColon {
|
|
buf.WriteByte(':')
|
|
}
|
|
|
|
buf.WriteString(param)
|
|
}
|
|
|
|
return buf.String()
|
|
}
|