refactor: shared service layer, default IRC port, smaller functions
All checks were successful
check / check (push) Successful in 2m37s
All checks were successful
check / check (push) Successful in 2m37s
Wire up service.Service in HTTP handlers and delegate cleanupUser to svc.BroadcastQuit for consistent quit/part logic across transports. Default IRC_LISTEN_ADDR to :6667, remove unused import, fix all lint issues (dogsled, funcorder, wrapcheck, varnamelen, nolintlint). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,6 @@ package ircserver
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
@@ -15,6 +14,7 @@ import (
|
||||
"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/service"
|
||||
"git.eeqj.de/sneak/neoirc/pkg/irc"
|
||||
)
|
||||
|
||||
@@ -30,6 +30,10 @@ const (
|
||||
minPasswordLen = 8
|
||||
)
|
||||
|
||||
// cmdHandler is the signature for registered IRC command
|
||||
// handlers.
|
||||
type cmdHandler func(ctx context.Context, msg *Message)
|
||||
|
||||
// Conn represents a single IRC client TCP connection.
|
||||
type Conn struct {
|
||||
conn net.Conn
|
||||
@@ -37,7 +41,9 @@ type Conn struct {
|
||||
database *db.Database
|
||||
brk *broker.Broker
|
||||
cfg *config.Config
|
||||
svc *service.Service
|
||||
serverSfx string
|
||||
commands map[string]cmdHandler
|
||||
|
||||
mu sync.Mutex
|
||||
nick string
|
||||
@@ -65,6 +71,7 @@ func newConn(
|
||||
database *db.Database,
|
||||
brk *broker.Broker,
|
||||
cfg *config.Config,
|
||||
svc *service.Service,
|
||||
) *Conn {
|
||||
host, _, _ := net.SplitHostPort(tcpConn.RemoteAddr().String())
|
||||
|
||||
@@ -73,16 +80,57 @@ func newConn(
|
||||
srvName = "neoirc"
|
||||
}
|
||||
|
||||
return &Conn{ //nolint:exhaustruct // zero-value defaults
|
||||
conn := &Conn{ //nolint:exhaustruct // zero-value defaults
|
||||
conn: tcpConn,
|
||||
log: log,
|
||||
database: database,
|
||||
brk: brk,
|
||||
cfg: cfg,
|
||||
svc: svc,
|
||||
serverSfx: srvName,
|
||||
remoteIP: host,
|
||||
hostname: resolveHost(ctx, host),
|
||||
}
|
||||
|
||||
conn.commands = conn.buildCommandMap()
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
// buildCommandMap returns a map from IRC command strings
|
||||
// to handler functions.
|
||||
func (c *Conn) buildCommandMap() map[string]cmdHandler {
|
||||
return map[string]cmdHandler{
|
||||
irc.CmdPing: func(_ context.Context, msg *Message) {
|
||||
c.handlePing(msg)
|
||||
},
|
||||
"PONG": func(context.Context, *Message) {},
|
||||
irc.CmdNick: c.handleNick,
|
||||
irc.CmdPrivmsg: c.handlePrivmsg,
|
||||
irc.CmdNotice: c.handlePrivmsg,
|
||||
irc.CmdJoin: c.handleJoin,
|
||||
irc.CmdPart: c.handlePart,
|
||||
irc.CmdQuit: func(_ context.Context, msg *Message) {
|
||||
c.handleQuit(msg)
|
||||
},
|
||||
irc.CmdTopic: c.handleTopic,
|
||||
irc.CmdMode: c.handleMode,
|
||||
irc.CmdNames: c.handleNames,
|
||||
irc.CmdList: func(ctx context.Context, _ *Message) { c.handleList(ctx) },
|
||||
irc.CmdWhois: c.handleWhois,
|
||||
irc.CmdWho: c.handleWho,
|
||||
irc.CmdLusers: func(ctx context.Context, _ *Message) { c.handleLusers(ctx) },
|
||||
irc.CmdMotd: func(context.Context, *Message) { c.deliverMOTD() },
|
||||
irc.CmdOper: c.handleOper,
|
||||
irc.CmdAway: c.handleAway,
|
||||
irc.CmdKick: c.handleKick,
|
||||
irc.CmdPass: c.handlePassPostReg,
|
||||
"INVITE": c.handleInvite,
|
||||
"CAP": func(_ context.Context, msg *Message) {
|
||||
c.handleCAP(msg)
|
||||
},
|
||||
"USERHOST": c.handleUserhost,
|
||||
}
|
||||
}
|
||||
|
||||
// resolveHost does a reverse DNS lookup, returning the IP
|
||||
@@ -145,71 +193,14 @@ func (c *Conn) cleanup(ctx context.Context) {
|
||||
c.mu.Unlock()
|
||||
|
||||
if wasRegistered && sessID > 0 {
|
||||
c.broadcastQuit(ctx, nick, "Connection closed")
|
||||
c.database.DeleteSession(ctx, sessID) //nolint:errcheck,gosec
|
||||
c.svc.BroadcastQuit(
|
||||
ctx, sessID, nick, "Connection closed",
|
||||
)
|
||||
}
|
||||
|
||||
c.conn.Close() //nolint:errcheck,gosec
|
||||
}
|
||||
|
||||
func (c *Conn) broadcastQuit(
|
||||
ctx context.Context,
|
||||
nick, reason string,
|
||||
) {
|
||||
channels, err := c.database.GetSessionChannels(
|
||||
ctx, c.sessionID,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
notified := make(map[int64]bool)
|
||||
|
||||
for _, ch := range channels {
|
||||
chID, getErr := c.database.GetChannelByName(
|
||||
ctx, ch.Name,
|
||||
)
|
||||
if getErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
memberIDs, memErr := c.database.GetChannelMemberIDs(
|
||||
ctx, chID,
|
||||
)
|
||||
if memErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, mid := range memberIDs {
|
||||
if mid == c.sessionID || notified[mid] {
|
||||
continue
|
||||
}
|
||||
|
||||
notified[mid] = true
|
||||
}
|
||||
}
|
||||
|
||||
body, _ := json.Marshal([]string{reason}) //nolint:errchkjson
|
||||
|
||||
for sid := range notified {
|
||||
dbID, _, insErr := c.database.InsertMessage(
|
||||
ctx, irc.CmdQuit, nick, "", nil, body, nil,
|
||||
)
|
||||
if insErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
_ = c.database.EnqueueToSession(ctx, sid, dbID)
|
||||
c.brk.Notify(sid)
|
||||
}
|
||||
|
||||
// Part from all channels so they get cleaned up.
|
||||
for _, ch := range channels {
|
||||
c.database.PartChannel(ctx, ch.ID, c.sessionID) //nolint:errcheck,gosec
|
||||
c.database.DeleteChannelIfEmpty(ctx, ch.ID) //nolint:errcheck,gosec
|
||||
}
|
||||
}
|
||||
|
||||
// send writes a formatted IRC line to the connection.
|
||||
func (c *Conn) send(line string) {
|
||||
_ = c.conn.SetWriteDeadline(
|
||||
@@ -261,9 +252,8 @@ func (c *Conn) hostmask() string {
|
||||
return c.nick + "!" + user + "@" + host
|
||||
}
|
||||
|
||||
// handleMessage dispatches a parsed IRC message.
|
||||
//
|
||||
//nolint:cyclop // dispatch table is inherently branchy
|
||||
// handleMessage dispatches a parsed IRC message using
|
||||
// the command handler map.
|
||||
func (c *Conn) handleMessage(
|
||||
ctx context.Context,
|
||||
msg *Message,
|
||||
@@ -276,57 +266,17 @@ func (c *Conn) handleMessage(
|
||||
return
|
||||
}
|
||||
|
||||
switch msg.Command {
|
||||
case irc.CmdPing:
|
||||
c.handlePing(msg)
|
||||
case "PONG":
|
||||
// Silently accept.
|
||||
case irc.CmdNick:
|
||||
c.handleNick(ctx, msg)
|
||||
case irc.CmdPrivmsg, irc.CmdNotice:
|
||||
c.handlePrivmsg(ctx, msg)
|
||||
case irc.CmdJoin:
|
||||
c.handleJoin(ctx, msg)
|
||||
case irc.CmdPart:
|
||||
c.handlePart(ctx, msg)
|
||||
case irc.CmdQuit:
|
||||
c.handleQuit(msg)
|
||||
case irc.CmdTopic:
|
||||
c.handleTopic(ctx, msg)
|
||||
case irc.CmdMode:
|
||||
c.handleMode(ctx, msg)
|
||||
case irc.CmdNames:
|
||||
c.handleNames(ctx, msg)
|
||||
case irc.CmdList:
|
||||
c.handleList(ctx)
|
||||
case irc.CmdWhois:
|
||||
c.handleWhois(ctx, msg)
|
||||
case irc.CmdWho:
|
||||
c.handleWho(ctx, msg)
|
||||
case irc.CmdLusers:
|
||||
c.handleLusers(ctx)
|
||||
case irc.CmdMotd:
|
||||
c.deliverMOTD()
|
||||
case irc.CmdOper:
|
||||
c.handleOper(ctx, msg)
|
||||
case irc.CmdAway:
|
||||
c.handleAway(ctx, msg)
|
||||
case irc.CmdKick:
|
||||
c.handleKick(ctx, msg)
|
||||
case irc.CmdPass:
|
||||
c.handlePassPostReg(ctx, msg)
|
||||
case "INVITE":
|
||||
c.handleInvite(ctx, msg)
|
||||
case "CAP":
|
||||
c.handleCAP(msg)
|
||||
case "USERHOST":
|
||||
c.handleUserhost(ctx, msg)
|
||||
default:
|
||||
handler, ok := c.commands[msg.Command]
|
||||
if !ok {
|
||||
c.sendNumeric(
|
||||
irc.ErrUnknownCommand,
|
||||
msg.Command, "Unknown command",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
handler(ctx, msg)
|
||||
}
|
||||
|
||||
// handlePreRegistration handles messages before the
|
||||
|
||||
Reference in New Issue
Block a user