All checks were successful
check / check (push) Successful in 5s
## Summary Adds a backward-compatible IRC wire protocol listener (RFC 1459/2812) that allows standard IRC clients (irssi, weechat, hexchat, etc.) to connect directly via TCP. ## Changes ### New package: `internal/ircserver/` - **`parser.go`** — IRC wire protocol message parser and formatter - **`server.go`** — TCP listener with Fx lifecycle integration - **`conn.go`** — Per-connection handler with registration flow, PING/PONG, welcome burst - **`commands.go`** — All IRC command handlers (JOIN, PART, PRIVMSG, MODE, TOPIC, KICK, WHOIS, etc.) - **`relay.go`** — Message relay goroutine that delivers queued messages to IRC clients in wire format ### Modified files - **`internal/config/config.go`** — Added `IRC_LISTEN_ADDR` environment variable - **`internal/handlers/handlers.go`** — Broker is now injected via Fx (shared with IRC server) - **`cmd/neoircd/main.go`** — Registered `broker.New`, `ircserver.New` as Fx providers - **`pkg/irc/commands.go`** — Added `CmdUser` and `CmdInvite` constants - **`README.md`** — Added IRC Protocol Listener documentation section ### Tests - Parser unit tests (table-driven, round-trip verification) - Integration tests: registration, PING/PONG, JOIN, PART, PRIVMSG (channel + DM), NICK change, duplicate nick rejection, LIST, WHOIS, QUIT, TOPIC, MODE, WHO, LUSERS, MOTD, AWAY, PASS, CAP negotiation, unknown commands, pre-registration errors - Benchmarks for parser and formatter ## Key Design Decisions - **Optional**: Listener is only started when `IRC_LISTEN_ADDR` is set - **Shared infrastructure**: Same DB, broker, and session system as HTTP API - **Full bridge**: IRC ↔ HTTP messages are interoperable - **No schema changes**: Reuses existing tables - **Broker as Fx dependency**: Extracted from handlers to be shared ## Supported Commands Connection: NICK, USER, PASS, QUIT, PING/PONG, CAP Channels: JOIN, PART, MODE, TOPIC, NAMES, LIST, KICK, INVITE Messaging: PRIVMSG, NOTICE Info: WHO, WHOIS, LUSERS, MOTD, AWAY, USERHOST Operator: OPER closes #89 Co-authored-by: user <user@Mac.lan guest wan> Co-authored-by: Jeffrey Paul <sneak@noreply.example.org> Reviewed-on: sneak/chat#94 Co-authored-by: clawbot <sneak+clawbot@sneak.cloud> Co-committed-by: clawbot <sneak+clawbot@sneak.cloud>
133 lines
4.0 KiB
Go
133 lines
4.0 KiB
Go
// Package config provides application configuration via environment and files.
|
|
package config
|
|
|
|
import (
|
|
"errors"
|
|
"log/slog"
|
|
|
|
"git.eeqj.de/sneak/neoirc/internal/globals"
|
|
"git.eeqj.de/sneak/neoirc/internal/logger"
|
|
"github.com/spf13/viper"
|
|
"go.uber.org/fx"
|
|
|
|
_ "github.com/joho/godotenv/autoload" // loads .env file
|
|
)
|
|
|
|
const defaultMOTD = ` _ __ ___ ___ (_)_ __ ___
|
|
| '_ \ / _ \/ _ \ | | '__/ __|
|
|
| | | | __/ (_) || | | | (__
|
|
|_| |_|\___|\___/ |_|_| \___|
|
|
|
|
Welcome to NeoIRC — IRC semantics over HTTP.
|
|
Type /help for available commands.`
|
|
|
|
// Params defines the dependencies for creating a Config.
|
|
type Params struct {
|
|
fx.In
|
|
|
|
Globals *globals.Globals
|
|
Logger *logger.Logger
|
|
}
|
|
|
|
// Config holds all application configuration values.
|
|
type Config struct {
|
|
DBURL string
|
|
Debug bool
|
|
MaintenanceMode bool
|
|
MetricsPassword string
|
|
MetricsUsername string
|
|
Port int
|
|
SentryDSN string
|
|
MessageMaxAge string
|
|
MaxMessageSize int
|
|
QueueMaxAge string
|
|
MOTD string
|
|
ServerName string
|
|
FederationKey string
|
|
SessionIdleTimeout string
|
|
HashcashBits int
|
|
OperName string
|
|
OperPassword string
|
|
LoginRateLimit float64
|
|
LoginRateBurst int
|
|
IRCListenAddr string
|
|
params *Params
|
|
log *slog.Logger
|
|
}
|
|
|
|
// New creates a new Config by reading from files and environment variables.
|
|
func New(
|
|
_ fx.Lifecycle, params Params,
|
|
) (*Config, error) {
|
|
log := params.Logger.Get()
|
|
name := params.Globals.Appname
|
|
|
|
viper.SetConfigName(name)
|
|
viper.SetConfigType("yaml")
|
|
viper.AddConfigPath("/etc/" + name)
|
|
viper.AddConfigPath("$HOME/.config/" + name)
|
|
viper.AutomaticEnv()
|
|
|
|
viper.SetDefault("DEBUG", "false")
|
|
viper.SetDefault("MAINTENANCE_MODE", "false")
|
|
viper.SetDefault("PORT", "8080")
|
|
viper.SetDefault("DBURL", "file:///var/lib/neoirc/state.db?_journal_mode=WAL")
|
|
viper.SetDefault("SENTRY_DSN", "")
|
|
viper.SetDefault("METRICS_USERNAME", "")
|
|
viper.SetDefault("METRICS_PASSWORD", "")
|
|
viper.SetDefault("MESSAGE_MAX_AGE", "720h")
|
|
viper.SetDefault("MAX_MESSAGE_SIZE", "4096")
|
|
viper.SetDefault("QUEUE_MAX_AGE", "720h")
|
|
viper.SetDefault("MOTD", defaultMOTD)
|
|
viper.SetDefault("SERVER_NAME", "")
|
|
viper.SetDefault("FEDERATION_KEY", "")
|
|
viper.SetDefault("SESSION_IDLE_TIMEOUT", "720h")
|
|
viper.SetDefault("NEOIRC_HASHCASH_BITS", "20")
|
|
viper.SetDefault("NEOIRC_OPER_NAME", "")
|
|
viper.SetDefault("NEOIRC_OPER_PASSWORD", "")
|
|
viper.SetDefault("LOGIN_RATE_LIMIT", "1")
|
|
viper.SetDefault("LOGIN_RATE_BURST", "5")
|
|
viper.SetDefault("IRC_LISTEN_ADDR", ":6667")
|
|
|
|
err := viper.ReadInConfig()
|
|
if err != nil {
|
|
var notFound viper.ConfigFileNotFoundError
|
|
if !errors.As(err, ¬Found) {
|
|
log.Error("config file malformed", "error", err)
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
cfg := &Config{
|
|
DBURL: viper.GetString("DBURL"),
|
|
Debug: viper.GetBool("DEBUG"),
|
|
Port: viper.GetInt("PORT"),
|
|
SentryDSN: viper.GetString("SENTRY_DSN"),
|
|
MaintenanceMode: viper.GetBool("MAINTENANCE_MODE"),
|
|
MetricsUsername: viper.GetString("METRICS_USERNAME"),
|
|
MetricsPassword: viper.GetString("METRICS_PASSWORD"),
|
|
MessageMaxAge: viper.GetString("MESSAGE_MAX_AGE"),
|
|
MaxMessageSize: viper.GetInt("MAX_MESSAGE_SIZE"),
|
|
QueueMaxAge: viper.GetString("QUEUE_MAX_AGE"),
|
|
MOTD: viper.GetString("MOTD"),
|
|
ServerName: viper.GetString("SERVER_NAME"),
|
|
FederationKey: viper.GetString("FEDERATION_KEY"),
|
|
SessionIdleTimeout: viper.GetString("SESSION_IDLE_TIMEOUT"),
|
|
HashcashBits: viper.GetInt("NEOIRC_HASHCASH_BITS"),
|
|
OperName: viper.GetString("NEOIRC_OPER_NAME"),
|
|
OperPassword: viper.GetString("NEOIRC_OPER_PASSWORD"),
|
|
LoginRateLimit: viper.GetFloat64("LOGIN_RATE_LIMIT"),
|
|
LoginRateBurst: viper.GetInt("LOGIN_RATE_BURST"),
|
|
IRCListenAddr: viper.GetString("IRC_LISTEN_ADDR"),
|
|
log: log,
|
|
params: ¶ms,
|
|
}
|
|
|
|
if cfg.Debug {
|
|
params.Logger.EnableDebugLogging()
|
|
cfg.log = params.Logger.Get()
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|