From 72d0c8d9bce125be1b851b142b0f41242d496f6f Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Sun, 06 Apr 2025 01:24:58 +0800 Subject: [PATCH] irc: Factor the IRC stuff into its own package --- config.go | 11 ++--------- git_hooks_handle_linux.go | 7 +------ git_hooks_handle_other.go | 7 +------ internal/irc/bot.go | 164 +++++++++++++++++++++++++++++++++++++++++++++++++++++ internal/misc/back.go | 6 ++++++ irc.go | 140 ----------------------------------------------------- server.go | 8 ++++---- diff --git a/config.go b/config.go index b8e5e28ae241656d01ba602d649907bd562ba941..d845ce88e215df7a08cd93287e43e487353140c3 100644 --- a/config.go +++ b/config.go @@ -10,6 +10,7 @@ "log/slog" "os" "go.lindenii.runxiyu.org/forge/internal/database" + "go.lindenii.runxiyu.org/forge/internal/irc" "go.lindenii.runxiyu.org/forge/internal/scfg" ) @@ -46,15 +47,7 @@ Addr string `scfg:"addr"` Key string `scfg:"key"` Root string `scfg:"root"` } `scfg:"ssh"` - IRC struct { - Net string `scfg:"net"` - Addr string `scfg:"addr"` - TLS bool `scfg:"tls"` - SendQ uint `scfg:"sendq"` - Nick string `scfg:"nick"` - User string `scfg:"user"` - Gecos string `scfg:"gecos"` - } `scfg:"irc"` + IRC irc.Config `scfg:"irc"` General struct { Title string `scfg:"title"` } `scfg:"general"` diff --git a/git_hooks_handle_linux.go b/git_hooks_handle_linux.go index aba47d3e10088417150458561dd4131dbb916602..fa4f86f198a5a799b57297ce78e6242d464f99e4 100644 --- a/git_hooks_handle_linux.go +++ b/git_hooks_handle_linux.go @@ -12,7 +12,6 @@ "encoding/binary" "errors" "fmt" "io" - "log/slog" "net" "os" "path/filepath" @@ -250,11 +249,7 @@ } mergeRequestWebURL := fmt.Sprintf("%s/contrib/%d/", s.genHTTPRemoteURL(packPass.groupPath, packPass.repoName), newMRLocalID) fmt.Fprintln(sshStderr, ansiec.Blue+"Created merge request at", mergeRequestWebURL+ansiec.Reset) - select { - case s.ircSendBuffered <- "PRIVMSG #chat :New merge request at " + mergeRequestWebURL: - default: - slog.Error("IRC SendQ exceeded") - } + s.ircBot.Send("PRIVMSG #chat :New merge request at " + mergeRequestWebURL) } else { // Existing contrib branch var existingMRUser int var isAncestor bool diff --git a/git_hooks_handle_other.go b/git_hooks_handle_other.go index d4a6aead791e5c11b20a9f2849c63385091dd469..da40bb68ed10f6dcabb254465e2ca396be9a8899 100644 --- a/git_hooks_handle_other.go +++ b/git_hooks_handle_other.go @@ -12,7 +12,6 @@ "encoding/binary" "errors" "fmt" "io" - "log/slog" "net" "path/filepath" "strconv" @@ -226,11 +225,7 @@ } mergeRequestWebURL := fmt.Sprintf("%s/contrib/%d/", s.genHTTPRemoteURL(packPass.groupPath, packPass.repoName), newMRLocalID) fmt.Fprintln(sshStderr, ansiec.Blue+"Created merge request at", mergeRequestWebURL+ansiec.Reset) - select { - case s.ircSendBuffered <- "PRIVMSG #chat :New merge request at " + mergeRequestWebURL: - default: - slog.Error("IRC SendQ exceeded") - } + s.ircBot.Send("PRIVMSG #chat :New merge request at " + mergeRequestWebURL) } else { // Existing contrib branch var existingMRUser int var isAncestor bool diff --git a/internal/irc/bot.go b/internal/irc/bot.go new file mode 100644 index 0000000000000000000000000000000000000000..6f7ba71f6c573fadb4d354fb9956c70e01a06ee8 --- /dev/null +++ b/internal/irc/bot.go @@ -0,0 +1,164 @@ +package irc + +import ( + "crypto/tls" + "log/slog" + "net" + + "go.lindenii.runxiyu.org/forge/internal/misc" + irc "go.lindenii.runxiyu.org/lindenii-irc" +) + +type Config struct { + Net string `scfg:"net"` + Addr string `scfg:"addr"` + TLS bool `scfg:"tls"` + SendQ uint `scfg:"sendq"` + Nick string `scfg:"nick"` + User string `scfg:"user"` + Gecos string `scfg:"gecos"` +} + +type Bot struct { + config *Config + ircSendBuffered chan string + ircSendDirectChan chan misc.ErrorBack[string] +} + +func NewBot(c *Config) (b *Bot) { + b = &Bot{ + config: c, + } + return +} + +func (b *Bot) Connect() error { + var err error + var underlyingConn net.Conn + if b.config.TLS { + underlyingConn, err = tls.Dial(b.config.Net, b.config.Addr, nil) + } else { + underlyingConn, err = net.Dial(b.config.Net, b.config.Addr) + } + if err != nil { + return err + } + defer underlyingConn.Close() + + conn := irc.NewConn(underlyingConn) + + logAndWriteLn := func(s string) (n int, err error) { + slog.Debug("irc tx", "line", s) + return conn.WriteString(s + "\r\n") + } + + _, err = logAndWriteLn("NICK " + b.config.Nick) + if err != nil { + return err + } + _, err = logAndWriteLn("USER " + b.config.User + " 0 * :" + b.config.Gecos) + if err != nil { + return err + } + + readLoopError := make(chan error) + writeLoopAbort := make(chan struct{}) + go func() { + for { + select { + case <-writeLoopAbort: + return + default: + } + + msg, line, err := conn.ReadMessage() + if err != nil { + readLoopError <- err + return + } + + slog.Debug("irc rx", "line", line) + + switch msg.Command { + case "001": + _, err = logAndWriteLn("JOIN #chat") + if err != nil { + readLoopError <- err + return + } + case "PING": + _, err = logAndWriteLn("PONG :" + msg.Args[0]) + if err != nil { + readLoopError <- err + return + } + case "JOIN": + c, ok := msg.Source.(irc.Client) + if !ok { + slog.Error("unable to convert source of JOIN to client") + } + if c.Nick != b.config.Nick { + continue + } + default: + } + } + }() + + for { + select { + case err = <-readLoopError: + return err + case line := <-b.ircSendBuffered: + _, err = logAndWriteLn(line) + if err != nil { + select { + case b.ircSendBuffered <- line: + default: + slog.Error("unable to requeue message", "line", line) + } + writeLoopAbort <- struct{}{} + return err + } + case lineErrorBack := <-b.ircSendDirectChan: + _, err = logAndWriteLn(lineErrorBack.Content) + lineErrorBack.ErrorChan <- err + if err != nil { + writeLoopAbort <- struct{}{} + return err + } + } + } +} + +// SendDirect sends an IRC message directly to the connection and bypasses +// the buffering system. +func (b *Bot) SendDirect(line string) error { + ech := make(chan error, 1) + + b.ircSendDirectChan <- misc.ErrorBack[string]{ + Content: line, + ErrorChan: ech, + } + + return <-ech +} + +func (b *Bot) Send(line string) { + select { + case b.ircSendBuffered <- line: + default: + slog.Error("irc sendq full", "line", line) + } +} + +// TODO: Delay and warnings? +func (b *Bot) ConnectLoop() { + b.ircSendBuffered = make(chan string, b.config.SendQ) + b.ircSendDirectChan = make(chan misc.ErrorBack[string]) + + for { + err := b.Connect() + slog.Error("irc session error", "error", err) + } +} diff --git a/internal/misc/back.go b/internal/misc/back.go new file mode 100644 index 0000000000000000000000000000000000000000..6b57e30d907aee8a9a4ae97fcbfb5e6973c28b3c --- /dev/null +++ b/internal/misc/back.go @@ -0,0 +1,6 @@ +package misc + +type ErrorBack[T any] struct { + Content T + ErrorChan chan error +} diff --git a/irc.go b/irc.go deleted file mode 100644 index 4f4c5c990ae06445d3b7054d97fc3955133da2e8..0000000000000000000000000000000000000000 --- a/irc.go +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu - -package forge - -import ( - "crypto/tls" - "log/slog" - "net" - - irc "go.lindenii.runxiyu.org/lindenii-irc" -) - -type errorBack[T any] struct { - content T - errorBack chan error -} - -func (s *Server) ircBotSession() error { - var err error - var underlyingConn net.Conn - if s.config.IRC.TLS { - underlyingConn, err = tls.Dial(s.config.IRC.Net, s.config.IRC.Addr, nil) - } else { - underlyingConn, err = net.Dial(s.config.IRC.Net, s.config.IRC.Addr) - } - if err != nil { - return err - } - defer underlyingConn.Close() - - conn := irc.NewConn(underlyingConn) - - logAndWriteLn := func(s string) (n int, err error) { - slog.Debug("irc tx", "line", s) - return conn.WriteString(s + "\r\n") - } - - _, err = logAndWriteLn("NICK " + s.config.IRC.Nick) - if err != nil { - return err - } - _, err = logAndWriteLn("USER " + s.config.IRC.User + " 0 * :" + s.config.IRC.Gecos) - if err != nil { - return err - } - - readLoopError := make(chan error) - writeLoopAbort := make(chan struct{}) - go func() { - for { - select { - case <-writeLoopAbort: - return - default: - } - - msg, line, err := conn.ReadMessage() - if err != nil { - readLoopError <- err - return - } - - slog.Debug("irc rx", "line", line) - - switch msg.Command { - case "001": - _, err = logAndWriteLn("JOIN #chat") - if err != nil { - readLoopError <- err - return - } - case "PING": - _, err = logAndWriteLn("PONG :" + msg.Args[0]) - if err != nil { - readLoopError <- err - return - } - case "JOIN": - c, ok := msg.Source.(irc.Client) - if !ok { - slog.Error("unable to convert source of JOIN to client") - } - if c.Nick != s.config.IRC.Nick { - continue - } - default: - } - } - }() - - for { - select { - case err = <-readLoopError: - return err - case line := <-s.ircSendBuffered: - _, err = logAndWriteLn(line) - if err != nil { - select { - case s.ircSendBuffered <- line: - default: - slog.Error("unable to requeue message", "line", line) - } - writeLoopAbort <- struct{}{} - return err - } - case lineErrorBack := <-s.ircSendDirectChan: - _, err = logAndWriteLn(lineErrorBack.content) - lineErrorBack.errorBack <- err - if err != nil { - writeLoopAbort <- struct{}{} - return err - } - } - } -} - -// ircSendDirect sends an IRC message directly to the connection and bypasses -// the buffering system. -func (s *Server) ircSendDirect(line string) error { - ech := make(chan error, 1) - - s.ircSendDirectChan <- errorBack[string]{ - content: line, - errorBack: ech, - } - - return <-ech -} - -// TODO: Delay and warnings? -func (s *Server) ircBotLoop() { - s.ircSendBuffered = make(chan string, s.config.IRC.SendQ) - s.ircSendDirectChan = make(chan errorBack[string]) - - for { - err := s.ircBotSession() - slog.Error("irc session error", "error", err) - } -} diff --git a/server.go b/server.go index 3e025867aee2da100598fe231c1e981b4af03865..8c14e3e0500aa02e82047344e851707061cc7887 100644 --- a/server.go +++ b/server.go @@ -18,6 +18,7 @@ "syscall" "time" "go.lindenii.runxiyu.org/forge/internal/database" + "go.lindenii.runxiyu.org/forge/internal/irc" "go.lindenii.runxiyu.org/forge/internal/misc" "go.lindenii.runxiyu.org/lindenii-common/cmap" goSSH "golang.org/x/crypto/ssh" @@ -31,9 +32,6 @@ sourceHandler http.Handler staticHandler http.Handler - ircSendBuffered chan string - ircSendDirectChan chan errorBack[string] - // globalData is passed as "global" when rendering HTML templates. globalData map[string]any @@ -45,6 +43,8 @@ // packPasses contains hook cookies mapped to their packPass. packPasses cmap.Map[string, packPass] templates *template.Template + + ircBot *irc.Bot } func (s *Server) Setup() { @@ -192,7 +192,7 @@ }() } // IRC bot - go s.ircBotLoop() + go s.ircBot.ConnectLoop() select {} } -- 2.48.1