feat: add IRC wire protocol listener with shared service layer
Some checks failed
check / check (push) Failing after 46s
Some checks failed
check / check (push) Failing after 46s
Add a traditional IRC wire protocol listener (RFC 1459/2812) on configurable port (default :6667), sharing business logic with the HTTP API via a new service layer. - IRC listener: NICK, USER, PASS, JOIN, PART, PRIVMSG, NOTICE, TOPIC, MODE, KICK, QUIT, NAMES, LIST, WHOIS, WHO, AWAY, OPER, INVITE, LUSERS, MOTD, PING/PONG, CAP - Service layer: shared logic for both transports including channel join (with Tier 2 checks: ban/invite/key/limit), message send (with ban + moderation checks), nick change, topic, kick, mode, quit broadcast, away, oper, invite - BroadcastQuit uses FanOut pattern (one insert, N enqueues) - HTTP handlers delegate to service for all command logic - Tier 2 mode operations (+b/+i/+s/+k/+l) use service methods Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
123
internal/ircserver/parser.go
Normal file
123
internal/ircserver/parser.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// 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()
|
||||
}
|
||||
Reference in New Issue
Block a user