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>
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()
|
|
}
|