From fbe0411756e5a9b9d6dccb6b8472500924899b2e Mon Sep 17 00:00:00 2001
From: Runxi Yu <me@runxiyu.org>
Date: Sat, 22 Mar 2025 10:38:18 +0800
Subject: [PATCH] IRC sending queues

---
 config.go  |  10 +++++++---
 forge.scfg |  10 ++++++++++
 irc.go     | 111 +++++++++++++++++++++++++++++++++++++++++++++--------

diff --git a/config.go b/config.go
index 9a5647f8d2328dd94510d598fad388f5fa74f990..5382bbed67ac077b1bbf7d3a8cf009541642a38d 100644
--- a/config.go
+++ b/config.go
@@ -36,9 +36,13 @@ 		Key  string `scfg:"key"`
 		Root string `scfg:"root"`
 	} `scfg:"ssh"`
 	IRC struct {
-		Net  string `scfg:"net"`
-		Addr string `scfg:"addr"`
-		TLS  bool   `scfg:"tls"`
+		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"`
 	General struct {
 		Title string `scfg:"title"`
diff --git a/forge.scfg b/forge.scfg
index cf77a4b9cbf457a3b10025d58ad00657b621d711..42a4b57264e44499c55e378946db7eacc40098f0 100644
--- a/forge.scfg
+++ b/forge.scfg
@@ -15,6 +15,16 @@ 	# What is the canonical URL of the web root?
 	root https://forge.example.org
 }
 
+irc {
+	tls true
+	net tcp
+	addr irc.runxiyu.org:6697
+	sendq 6000
+	nick forge-test
+	user forge
+	gecos "Lindenii Forge Test"
+}
+
 git {
 	repo_dir /var/lib/lindenii/forge/repos
 }
diff --git a/irc.go b/irc.go
index fc9e0af39e22cd41875d2e25b4c321cee53d3223..afc9f965b6d99a0641ce6a209f836b49b0c3e159 100644
--- a/irc.go
+++ b/irc.go
@@ -4,9 +4,20 @@ import (
 	"crypto/tls"
 	"net"
 
+	"go.lindenii.runxiyu.org/lindenii-common/clog"
 	"go.lindenii.runxiyu.org/lindenii-irc"
 )
 
+var (
+	ircSendBuffered   chan string
+	ircSendDirectChan chan errorBack[string]
+)
+
+type errorBack[T any] struct {
+	content   T
+	errorBack chan error
+}
+
 func ircBotSession() error {
 	var err error
 	var underlyingConn net.Conn
@@ -16,31 +27,97 @@ 	} else {
 		underlyingConn, err = net.Dial(config.IRC.Net, config.IRC.Addr)
 	}
 	if err != nil {
-		return (err)
+		return err
 	}
+	defer underlyingConn.Close()
+
 	conn := irc.NewConn(underlyingConn)
-	conn.WriteString("NICK forge\r\nUSER forge 0 * :Forge\r\n")
+	conn.WriteString(
+		"NICK " + config.IRC.Nick + "\r\n" +
+			"USER " + config.IRC.User + " 0 * :" + config.IRC.Gecos + "\r\n",
+	)
+
+	readLoopError := make(chan error)
+	writeLoopAbort := make(chan struct{})
+	go func() {
+		for {
+			select {
+			case <-writeLoopAbort:
+				return
+			default:
+			}
+			msg, err := conn.ReadMessage()
+			if err != nil {
+				readLoopError <- err
+				return
+			}
+			switch msg.Command {
+			case "001":
+				_, err = conn.WriteString("JOIN #chat\r\n")
+				if err != nil {
+					readLoopError <- err
+					return
+				}
+			case "PING":
+				_, err = conn.WriteString("PONG :" + msg.Args[0] + "\r\n")
+				if err != nil {
+					readLoopError <- err
+					return
+				}
+			case "JOIN":
+				_, err = conn.WriteString("PRIVMSG #chat :test\r\n")
+				if err != nil {
+					readLoopError <- err
+					return
+				}
+			default:
+			}
+		}
+	}()
+
 	for {
-		msg, err := conn.ReadMessage()
-		if err != nil {
-			return (err)
+		select {
+		case err = <-readLoopError:
+			return err
+		case s := <-ircSendBuffered:
+			_, err = conn.WriteString(s)
+			if err != nil {
+				select {
+				case ircSendBuffered <- s:
+				default:
+					clog.Error("unable to requeue IRC message: " + s)
+				}
+				writeLoopAbort <- struct{}{}
+				return err
+			}
+		case se := <-ircSendDirectChan:
+			_, err = conn.WriteString(se.content)
+			se.errorBack <- err
+			if err != nil {
+				writeLoopAbort <- struct{}{}
+				return err
+			}
 		}
-		switch msg.Command {
-		case "001":
-			conn.WriteString("JOIN #chat\r\n")
-		case "PING":
-			conn.WriteString("PONG :")
-			conn.WriteString(msg.Args[0])
-			conn.WriteString("\r\n")
-		case "JOIN":
-			conn.WriteString("PRIVMSG #chat :test\r\n")
-		default:
-		}
+	}
+}
+
+func ircSendDirect(s string) error {
+	ech := make(chan error, 1)
+
+	ircSendDirectChan <- errorBack[string]{
+		content:   s,
+		errorBack: ech,
 	}
+
+	return <-ech
 }
 
 func ircBotLoop() {
+	ircSendBuffered = make(chan string, config.IRC.SendQ)
+	ircSendDirectChan = make(chan errorBack[string])
+
 	for {
-		_ = ircBotSession()
+		err := ircBotSession()
+		clog.Error("IRC error: " + err.Error())
 	}
 }

-- 
2.48.1