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>
158 lines
2.9 KiB
Go
158 lines
2.9 KiB
Go
package ircserver
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"sync"
|
|
|
|
"git.eeqj.de/sneak/neoirc/internal/broker"
|
|
"git.eeqj.de/sneak/neoirc/internal/config"
|
|
"git.eeqj.de/sneak/neoirc/internal/db"
|
|
"git.eeqj.de/sneak/neoirc/internal/logger"
|
|
"git.eeqj.de/sneak/neoirc/internal/service"
|
|
"go.uber.org/fx"
|
|
)
|
|
|
|
// Params defines the dependencies for creating an IRC
|
|
// Server.
|
|
type Params struct {
|
|
fx.In
|
|
|
|
Logger *logger.Logger
|
|
Config *config.Config
|
|
Database *db.Database
|
|
Broker *broker.Broker
|
|
Service *service.Service
|
|
}
|
|
|
|
// Server is the TCP IRC protocol server.
|
|
type Server struct {
|
|
log *slog.Logger
|
|
cfg *config.Config
|
|
database *db.Database
|
|
brk *broker.Broker
|
|
svc *service.Service
|
|
listener net.Listener
|
|
mu sync.Mutex
|
|
conns map[*Conn]struct{}
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// New creates a new IRC Server and registers its lifecycle
|
|
// hooks. The listener is only started if IRC_LISTEN_ADDR
|
|
// is configured; otherwise the server is inert.
|
|
func New(
|
|
lifecycle fx.Lifecycle,
|
|
params Params,
|
|
) *Server {
|
|
srv := &Server{
|
|
log: params.Logger.Get(),
|
|
cfg: params.Config,
|
|
database: params.Database,
|
|
brk: params.Broker,
|
|
svc: params.Service,
|
|
conns: make(map[*Conn]struct{}),
|
|
listener: nil,
|
|
cancel: nil,
|
|
mu: sync.Mutex{},
|
|
}
|
|
|
|
listenAddr := params.Config.IRCListenAddr
|
|
if listenAddr == "" {
|
|
return srv
|
|
}
|
|
|
|
lifecycle.Append(fx.Hook{
|
|
OnStart: func(ctx context.Context) error {
|
|
return srv.start(ctx, listenAddr)
|
|
},
|
|
OnStop: func(_ context.Context) error {
|
|
srv.stop()
|
|
|
|
return nil
|
|
},
|
|
})
|
|
|
|
return srv
|
|
}
|
|
|
|
// start begins listening for TCP connections.
|
|
//
|
|
//nolint:contextcheck // long-lived server ctx, not the short Fx one
|
|
func (s *Server) start(_ context.Context, addr string) error {
|
|
ln, err := net.Listen("tcp", addr)
|
|
if err != nil {
|
|
return fmt.Errorf("irc listen: %w", err)
|
|
}
|
|
|
|
s.listener = ln
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
s.cancel = cancel
|
|
|
|
s.log.Info(
|
|
"irc server listening", "addr", addr,
|
|
)
|
|
|
|
go s.acceptLoop(ctx)
|
|
|
|
return nil
|
|
}
|
|
|
|
// stop shuts down the listener and all connections.
|
|
func (s *Server) stop() {
|
|
if s.cancel != nil {
|
|
s.cancel()
|
|
}
|
|
|
|
if s.listener != nil {
|
|
s.listener.Close() //nolint:errcheck,gosec
|
|
}
|
|
|
|
s.mu.Lock()
|
|
for c := range s.conns {
|
|
c.conn.Close() //nolint:errcheck,gosec
|
|
}
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
// acceptLoop accepts new connections.
|
|
func (s *Server) acceptLoop(ctx context.Context) {
|
|
for {
|
|
tcpConn, err := s.listener.Accept()
|
|
if err != nil {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
s.log.Error(
|
|
"irc accept error", "error", err,
|
|
)
|
|
|
|
continue
|
|
}
|
|
}
|
|
|
|
client := newConn(
|
|
ctx, tcpConn, s.log,
|
|
s.database, s.brk, s.cfg, s.svc,
|
|
)
|
|
|
|
s.mu.Lock()
|
|
s.conns[client] = struct{}{}
|
|
s.mu.Unlock()
|
|
|
|
go func() {
|
|
defer func() {
|
|
s.mu.Lock()
|
|
delete(s.conns, client)
|
|
s.mu.Unlock()
|
|
}()
|
|
|
|
client.serve(ctx)
|
|
}()
|
|
}
|
|
}
|