fix: resolve cyclop, funlen issues by extracting helper methods

This commit is contained in:
user
2026-02-20 03:29:18 -08:00
parent 037202280b
commit 2f6d1f284c
5 changed files with 263 additions and 248 deletions

View File

@@ -97,6 +97,24 @@ func (a *App) handleInput(text string) {
a.ui.AddLine(target, fmt.Sprintf("[gray]%s [green]<%s>[white] %s", ts, nick, text)) a.ui.AddLine(target, fmt.Sprintf("[gray]%s [green]<%s>[white] %s", ts, nick, text))
} }
func (a *App) commandHandlers() map[string]func(string) {
return map[string]func(string){
"/connect": a.cmdConnect,
"/nick": a.cmdNick,
"/join": a.cmdJoin,
"/part": a.cmdPart,
"/msg": a.cmdMsg,
"/query": a.cmdQuery,
"/topic": a.cmdTopic,
"/names": func(_ string) { a.cmdNames() },
"/list": func(_ string) { a.cmdList() },
"/window": a.cmdWindow,
"/w": a.cmdWindow,
"/quit": func(_ string) { a.cmdQuit() },
"/help": func(_ string) { a.cmdHelp() },
}
}
func (a *App) handleCommand(text string) { func (a *App) handleCommand(text string) {
parts := strings.SplitN(text, " ", splitParts) parts := strings.SplitN(text, " ", splitParts)
cmd := strings.ToLower(parts[0]) cmd := strings.ToLower(parts[0])
@@ -106,32 +124,9 @@ func (a *App) handleCommand(text string) {
args = parts[1] args = parts[1]
} }
switch cmd { if handler, ok := a.commandHandlers()[cmd]; ok {
case "/connect": handler(args)
a.cmdConnect(args) } else {
case "/nick":
a.cmdNick(args)
case "/join":
a.cmdJoin(args)
case "/part":
a.cmdPart(args)
case "/msg":
a.cmdMsg(args)
case "/query":
a.cmdQuery(args)
case "/topic":
a.cmdTopic(args)
case "/names":
a.cmdNames()
case "/list":
a.cmdList()
case "/window", "/w":
a.cmdWindow(args)
case "/quit":
a.cmdQuit()
case "/help":
a.cmdHelp()
default:
a.ui.AddStatus("[red]Unknown command: " + cmd) a.ui.AddStatus("[red]Unknown command: " + cmd)
} }
} }
@@ -543,68 +538,90 @@ func (a *App) pollLoop() {
} }
} }
func (a *App) handleServerMessage(msg *api.Message) { func (a *App) messageTimestamp(msg *api.Message) string {
var ts string
if msg.TS != "" { if msg.TS != "" {
t := msg.ParseTS() t := msg.ParseTS()
ts = t.Local().Format("15:04") //nolint:gosmopolitan // CLI displays local time intentionally
} else { return t.Local().Format("15:04") //nolint:gosmopolitan // CLI displays local time intentionally
ts = time.Now().Format("15:04")
} }
return time.Now().Format("15:04")
}
func (a *App) handleServerMessage(msg *api.Message) {
ts := a.messageTimestamp(msg)
a.mu.Lock() a.mu.Lock()
myNick := a.nick myNick := a.nick
a.mu.Unlock() a.mu.Unlock()
switch msg.Command { switch msg.Command {
case "PRIVMSG": case "PRIVMSG":
a.handleMsgPrivmsg(msg, ts, myNick)
case "JOIN":
a.handleMsgJoin(msg, ts)
case "PART":
a.handleMsgPart(msg, ts)
case "QUIT":
a.handleMsgQuit(msg, ts)
case "NICK":
a.handleMsgNick(msg, ts, myNick)
case "NOTICE":
a.handleMsgNotice(msg, ts)
case "TOPIC":
a.handleMsgTopic(msg, ts)
default:
a.handleMsgDefault(msg, ts)
}
}
func (a *App) handleMsgPrivmsg(msg *api.Message, ts, myNick string) {
lines := msg.BodyLines() lines := msg.BodyLines()
text := strings.Join(lines, " ") text := strings.Join(lines, " ")
if msg.From == myNick { if msg.From == myNick {
// Skip our own echoed messages (already displayed locally).
return return
} }
target := msg.To target := msg.To
if !strings.HasPrefix(target, "#") { if !strings.HasPrefix(target, "#") {
// DM — use sender's nick as buffer name.
target = msg.From target = msg.From
} }
a.ui.AddLine(target, fmt.Sprintf("[gray]%s [green]<%s>[white] %s", ts, msg.From, text)) a.ui.AddLine(target, fmt.Sprintf("[gray]%s [green]<%s>[white] %s", ts, msg.From, text))
}
case "JOIN": func (a *App) handleMsgJoin(msg *api.Message, ts string) {
if msg.To != "" {
a.ui.AddLine(msg.To, fmt.Sprintf("[gray]%s [yellow]*** %s has joined %s", ts, msg.From, msg.To))
}
}
func (a *App) handleMsgPart(msg *api.Message, ts string) {
target := msg.To target := msg.To
if target != "" { reason := strings.Join(msg.BodyLines(), " ")
a.ui.AddLine(target, fmt.Sprintf("[gray]%s [yellow]*** %s has joined %s", ts, msg.From, target))
if target == "" {
return
} }
case "PART":
target := msg.To
lines := msg.BodyLines()
reason := strings.Join(lines, " ")
if target != "" {
if reason != "" { if reason != "" {
a.ui.AddLine(target, fmt.Sprintf("[gray]%s [yellow]*** %s has left %s (%s)", ts, msg.From, target, reason)) a.ui.AddLine(target, fmt.Sprintf("[gray]%s [yellow]*** %s has left %s (%s)", ts, msg.From, target, reason))
} else { } else {
a.ui.AddLine(target, fmt.Sprintf("[gray]%s [yellow]*** %s has left %s", ts, msg.From, target)) a.ui.AddLine(target, fmt.Sprintf("[gray]%s [yellow]*** %s has left %s", ts, msg.From, target))
} }
} }
case "QUIT": func (a *App) handleMsgQuit(msg *api.Message, ts string) {
lines := msg.BodyLines() reason := strings.Join(msg.BodyLines(), " ")
reason := strings.Join(lines, " ")
if reason != "" { if reason != "" {
a.ui.AddStatus(fmt.Sprintf("[gray]%s [yellow]*** %s has quit (%s)", ts, msg.From, reason)) a.ui.AddStatus(fmt.Sprintf("[gray]%s [yellow]*** %s has quit (%s)", ts, msg.From, reason))
} else { } else {
a.ui.AddStatus(fmt.Sprintf("[gray]%s [yellow]*** %s has quit", ts, msg.From)) a.ui.AddStatus(fmt.Sprintf("[gray]%s [yellow]*** %s has quit", ts, msg.From))
} }
}
case "NICK": func (a *App) handleMsgNick(msg *api.Message, ts, myNick string) {
lines := msg.BodyLines() lines := msg.BodyLines()
newNick := "" newNick := ""
@@ -621,27 +638,23 @@ func (a *App) handleServerMessage(msg *api.Message) {
} }
a.ui.AddStatus(fmt.Sprintf("[gray]%s [yellow]*** %s is now known as %s", ts, msg.From, newNick)) a.ui.AddStatus(fmt.Sprintf("[gray]%s [yellow]*** %s is now known as %s", ts, msg.From, newNick))
}
case "NOTICE": func (a *App) handleMsgNotice(msg *api.Message, ts string) {
lines := msg.BodyLines() text := strings.Join(msg.BodyLines(), " ")
text := strings.Join(lines, " ")
a.ui.AddStatus(fmt.Sprintf("[gray]%s [magenta]--%s-- %s", ts, msg.From, text)) a.ui.AddStatus(fmt.Sprintf("[gray]%s [magenta]--%s-- %s", ts, msg.From, text))
}
case "TOPIC": func (a *App) handleMsgTopic(msg *api.Message, ts string) {
lines := msg.BodyLines() text := strings.Join(msg.BodyLines(), " ")
text := strings.Join(lines, " ")
if msg.To != "" { if msg.To != "" {
a.ui.AddLine(msg.To, fmt.Sprintf("[gray]%s [cyan]*** %s set topic: %s", ts, msg.From, text)) a.ui.AddLine(msg.To, fmt.Sprintf("[gray]%s [cyan]*** %s set topic: %s", ts, msg.From, text))
} }
}
default: func (a *App) handleMsgDefault(msg *api.Message, ts string) {
// Numeric replies and other messages → status window. text := strings.Join(msg.BodyLines(), " ")
lines := msg.BodyLines()
text := strings.Join(lines, " ")
if text != "" { if text != "" {
a.ui.AddStatus(fmt.Sprintf("[gray]%s [white][%s] %s", ts, msg.Command, text)) a.ui.AddStatus(fmt.Sprintf("[gray]%s [white][%s] %s", ts, msg.Command, text))
} }
}
} }

View File

@@ -55,12 +55,30 @@ func NewUI() *UI {
ui.statusBar.SetBackgroundColor(tcell.ColorNavy) ui.statusBar.SetBackgroundColor(tcell.ColorNavy)
ui.statusBar.SetTextColor(tcell.ColorWhite) ui.statusBar.SetTextColor(tcell.ColorWhite)
// Input field. ui.setupInput()
ui.setupKeyCapture()
// Layout: messages on top, status bar, input at bottom.
ui.layout = tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(ui.messages, 0, 1, false).
AddItem(ui.statusBar, 1, 0, false).
AddItem(ui.input, 1, 0, true)
ui.app.SetRoot(ui.layout, true)
ui.app.SetFocus(ui.input)
return ui
}
func (ui *UI) setupInput() {
ui.input = tview.NewInputField(). ui.input = tview.NewInputField().
SetFieldBackgroundColor(tcell.ColorBlack). SetFieldBackgroundColor(tcell.ColorBlack).
SetFieldTextColor(tcell.ColorWhite) SetFieldTextColor(tcell.ColorWhite)
ui.input.SetDoneFunc(func(key tcell.Key) { ui.input.SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEnter { if key != tcell.KeyEnter {
return
}
text := ui.input.GetText() text := ui.input.GetText()
if text == "" { if text == "" {
return return
@@ -71,10 +89,10 @@ func NewUI() *UI {
if ui.onInput != nil { if ui.onInput != nil {
ui.onInput(text) ui.onInput(text)
} }
}
}) })
}
// Capture Alt+N for window switching. func (ui *UI) setupKeyCapture() {
ui.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { ui.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Modifiers()&tcell.ModAlt != 0 { if event.Modifiers()&tcell.ModAlt != 0 {
r := event.Rune() r := event.Rune()
@@ -88,17 +106,6 @@ func NewUI() *UI {
return event return event
}) })
// Layout: messages on top, status bar, input at bottom.
ui.layout = tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(ui.messages, 0, 1, false).
AddItem(ui.statusBar, 1, 0, false).
AddItem(ui.input, 1, 0, true)
ui.app.SetRoot(ui.layout, true)
ui.app.SetFocus(ui.input)
return ui
} }
// Run starts the UI event loop (blocks). // Run starts the UI event loop (blocks).

View File

@@ -662,6 +662,15 @@ func (s *Database) applyMigrations(
migrations []migration, migrations []migration,
) error { ) error {
for _, m := range migrations { for _, m := range migrations {
if err := s.applyOneMigration(ctx, m); err != nil {
return err
}
}
return nil
}
func (s *Database) applyOneMigration(ctx context.Context, m migration) error {
var exists int var exists int
err := s.db.QueryRowContext(ctx, err := s.db.QueryRowContext(ctx,
@@ -669,35 +678,25 @@ func (s *Database) applyMigrations(
m.version, m.version,
).Scan(&exists) ).Scan(&exists)
if err != nil { if err != nil {
return fmt.Errorf( return fmt.Errorf("check migration %d: %w", m.version, err)
"check migration %d: %w", m.version, err,
)
} }
if exists > 0 { if exists > 0 {
continue return nil
} }
s.log.Info( s.log.Info("applying migration", "version", m.version, "name", m.name)
"applying migration",
"version", m.version, "name", m.name,
)
tx, err := s.db.BeginTx(ctx, nil) tx, err := s.db.BeginTx(ctx, nil)
if err != nil { if err != nil {
return fmt.Errorf( return fmt.Errorf("begin tx for migration %d: %w", m.version, err)
"begin tx for migration %d: %w", m.version, err,
)
} }
_, err = tx.ExecContext(ctx, m.sql) _, err = tx.ExecContext(ctx, m.sql)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
return fmt.Errorf( return fmt.Errorf("apply migration %d (%s): %w", m.version, m.name, err)
"apply migration %d (%s): %w",
m.version, m.name, err,
)
} }
_, err = tx.ExecContext(ctx, _, err = tx.ExecContext(ctx,
@@ -707,18 +706,8 @@ func (s *Database) applyMigrations(
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
return fmt.Errorf( return fmt.Errorf("record migration %d: %w", m.version, err)
"record migration %d: %w", m.version, err,
)
} }
err = tx.Commit() return tx.Commit()
if err != nil {
return fmt.Errorf(
"commit migration %d: %w", m.version, err,
)
}
}
return nil
} }

View File

@@ -239,10 +239,15 @@ func (s *Handlers) HandleSendCommand() http.HandlerFunc {
req.Command = strings.ToUpper(strings.TrimSpace(req.Command)) req.Command = strings.ToUpper(strings.TrimSpace(req.Command))
req.To = strings.TrimSpace(req.To) req.To = strings.TrimSpace(req.To)
lines := extractBodyLines(req.Body)
// Helper to extract body as string lines. s.dispatchCommand(w, r, uid, nick, req.Command, req.To, lines)
bodyLines := func() []string { }
switch v := req.Body.(type) { }
// extractBodyLines converts the request body to string lines.
func extractBodyLines(body any) []string {
switch v := body.(type) {
case []any: case []any:
lines := make([]string, 0, len(v)) lines := make([]string, 0, len(v))
@@ -258,32 +263,29 @@ func (s *Handlers) HandleSendCommand() http.HandlerFunc {
default: default:
return nil return nil
} }
} }
switch req.Command { func (s *Handlers) dispatchCommand(
w http.ResponseWriter, r *http.Request,
uid, nick, command, to string, lines []string,
) {
switch command {
case "PRIVMSG", "NOTICE": case "PRIVMSG", "NOTICE":
s.handlePrivmsg(w, r, uid, nick, req.To, bodyLines()) s.handlePrivmsg(w, r, uid, nick, to, lines)
case "JOIN": case "JOIN":
s.handleJoin(w, r, uid, req.To) s.handleJoin(w, r, uid, to)
case "PART": case "PART":
s.handlePart(w, r, uid, req.To) s.handlePart(w, r, uid, to)
case "NICK": case "NICK":
s.handleNick(w, r, uid, bodyLines()) s.handleNick(w, r, uid, lines)
case "TOPIC": case "TOPIC":
s.handleTopic(w, r, uid, req.To, bodyLines()) s.handleTopic(w, r, uid, to, lines)
case "PING": case "PING":
s.respondJSON(w, r, map[string]string{"command": "PONG", "from": s.params.Config.ServerName}, http.StatusOK) s.respondJSON(w, r, map[string]string{"command": "PONG", "from": s.params.Config.ServerName}, http.StatusOK)
default: default:
_ = nick // suppress unused warning _ = nick
s.respondJSON(w, r, map[string]string{"error": "unknown command: " + req.Command}, http.StatusBadRequest) s.respondJSON(w, r, map[string]string{"error": "unknown command: " + command}, http.StatusBadRequest)
}
} }
} }

View File

@@ -65,11 +65,17 @@ func (s *Server) SetupRoutes() {
r.Get("/channels/{channel}/members", s.h.HandleChannelMembers()) r.Get("/channels/{channel}/members", s.h.HandleChannelMembers())
}) })
// Serve embedded SPA s.setupSPA()
}
func (s *Server) setupSPA() {
distFS, err := fs.Sub(web.Dist, "dist") distFS, err := fs.Sub(web.Dist, "dist")
if err != nil { if err != nil {
s.log.Error("failed to get web dist filesystem", "error", err) s.log.Error("failed to get web dist filesystem", "error", err)
} else {
return
}
fileServer := http.FileServer(http.FS(distFS)) fileServer := http.FileServer(http.FS(distFS))
s.router.Get("/*", func(w http.ResponseWriter, r *http.Request) { s.router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
@@ -80,7 +86,6 @@ func (s *Server) SetupRoutes() {
return return
} }
// Try to serve the file; if not found, serve index.html for SPA routing
f, err := readFS.ReadFile(r.URL.Path[1:]) f, err := readFS.ReadFile(r.URL.Path[1:])
if err != nil || len(f) == 0 { if err != nil || len(f) == 0 {
indexHTML, _ := readFS.ReadFile("index.html") indexHTML, _ := readFS.ReadFile("index.html")
@@ -94,5 +99,4 @@ func (s *Server) SetupRoutes() {
fileServer.ServeHTTP(w, r) fileServer.ServeHTTP(w, r)
}) })
}
} }