Lindenii Project Forge
Move all config typedefs to config.go
package database type Config struct { Conn string `scfg:"conn"` }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> // Package database provides stubs and wrappers for databases. package database import ( "context" "fmt" "github.com/jackc/pgx/v5/pgxpool" ) // Database is a wrapper around pgxpool.Pool to provide a common interface for // other packages in the forge. type Database struct { *pgxpool.Pool } // Open opens a new database connection pool using the provided connection // string. It returns a Database instance and an error if any occurs. // It is run indefinitely in the background. func Open(ctx context.Context, config Config) (Database, error) { db, err := pgxpool.New(ctx, config.Conn) if err != nil { err = fmt.Errorf("create pgxpool: %w", err) } return Database{db}, err }
type Config struct { Conn string `scfg:"conn"` }
package hooks type Config struct { Socket string `scfg:"socket"` Execs string `scfg:"execs"` }
package hooks import ( "context" "errors" "fmt" "net" "time" "github.com/gliderlabs/ssh" "go.lindenii.runxiyu.org/forge/forged/internal/common/cmap" "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" ) type Server struct { hookMap cmap.Map[string, hookInfo] socketPath string executablesPath string }
type Config struct { Socket string `scfg:"socket"` Execs string `scfg:"execs"` }
type hookInfo struct { session ssh.Session pubkey string directAccess bool repoPath string userID int userType string repoID int groupPath []string repoName string contribReq string } func New(config Config) (server *Server) { return &Server{ socketPath: config.Socket, executablesPath: config.Execs, hookMap: cmap.Map[string, hookInfo]{}, } } func (server *Server) Run(ctx context.Context) error { listener, _, err := misc.ListenUnixSocket(ctx, server.socketPath) if err != nil { return fmt.Errorf("listen unix socket for hooks: %w", err) } defer func() { _ = listener.Close() }() stop := context.AfterFunc(ctx, func() { _ = listener.Close() }) defer stop() for { conn, err := listener.Accept() if err != nil { if errors.Is(err, net.ErrClosed) || ctx.Err() != nil { return nil } return fmt.Errorf("accept conn: %w", err) } go server.handleConn(ctx, conn) } } func (server *Server) handleConn(ctx context.Context, conn net.Conn) { defer func() { _ = conn.Close() }() unblock := context.AfterFunc(ctx, func() { _ = conn.SetDeadline(time.Now()) _ = conn.Close() }) defer unblock() }
package lmtp type Config struct { Socket string `scfg:"socket"` Domain string `scfg:"domain"` MaxSize int64 `scfg:"max_size"` WriteTimeout uint32 `scfg:"write_timeout"` ReadTimeout uint32 `scfg:"read_timeout"` }
package lmtp import ( "context" "errors" "fmt" "net" "time" "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" ) type Server struct { socket string domain string maxSize int64 writeTimeout uint32 readTimeout uint32 }
type Config struct { Socket string `scfg:"socket"` Domain string `scfg:"domain"` MaxSize int64 `scfg:"max_size"` WriteTimeout uint32 `scfg:"write_timeout"` ReadTimeout uint32 `scfg:"read_timeout"` }
func New(config Config) (server *Server) { return &Server{ socket: config.Socket, domain: config.Domain, maxSize: config.MaxSize, writeTimeout: config.WriteTimeout, readTimeout: config.ReadTimeout, } } func (server *Server) Run(ctx context.Context) error { listener, _, err := misc.ListenUnixSocket(ctx, server.socket) if err != nil { return fmt.Errorf("listen unix socket for LMTP: %w", err) } defer func() { _ = listener.Close() }() stop := context.AfterFunc(ctx, func() { _ = listener.Close() }) defer stop() for { conn, err := listener.Accept() if err != nil { if errors.Is(err, net.ErrClosed) || ctx.Err() != nil { return nil } return fmt.Errorf("accept conn: %w", err) } go server.handleConn(ctx, conn) } } func (server *Server) handleConn(ctx context.Context, conn net.Conn) { defer func() { _ = conn.Close() }() unblock := context.AfterFunc(ctx, func() { _ = conn.SetDeadline(time.Now()) _ = conn.Close() }) defer unblock() }
package ssh type Config struct { Net string `scfg:"net"` Addr string `scfg:"addr"` Key string `scfg:"key"` Root string `scfg:"root"` ShutdownTimeout uint32 `scfg:"shutdown_timeout"` }
package ssh import ( "context" "errors" "fmt" "os" "time" gliderssh "github.com/gliderlabs/ssh" "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" gossh "golang.org/x/crypto/ssh" )
type Config struct { Net string `scfg:"net"` Addr string `scfg:"addr"` Key string `scfg:"key"` Root string `scfg:"root"` ShutdownTimeout uint32 `scfg:"shutdown_timeout"` }
type Server struct { gliderServer *gliderssh.Server privkey gossh.Signer pubkeyString string pubkeyFP string net string addr string root string shutdownTimeout uint32 } func New(config Config) (server *Server, err error) { server = &Server{ net: config.Net, addr: config.Addr, root: config.Root, shutdownTimeout: config.ShutdownTimeout, } //exhaustruct:ignore var privkeyBytes []byte privkeyBytes, err = os.ReadFile(config.Key) if err != nil { return server, fmt.Errorf("read SSH private key: %w", err) } server.privkey, err = gossh.ParsePrivateKey(privkeyBytes) if err != nil { return server, fmt.Errorf("parse SSH private key: %w", err) } server.pubkeyString = misc.BytesToString(gossh.MarshalAuthorizedKey(server.privkey.PublicKey())) server.pubkeyFP = gossh.FingerprintSHA256(server.privkey.PublicKey()) server.gliderServer = &gliderssh.Server{ Handler: handle, PublicKeyHandler: func(ctx gliderssh.Context, key gliderssh.PublicKey) bool { return true }, KeyboardInteractiveHandler: func(ctx gliderssh.Context, challenge gossh.KeyboardInteractiveChallenge) bool { return true }, } //exhaustruct:ignore server.gliderServer.AddHostKey(server.privkey) return server, nil } func (server *Server) Run(ctx context.Context) (err error) { listener, err := misc.Listen(ctx, server.net, server.addr) if err != nil { return fmt.Errorf("listen for SSH: %w", err) } defer func() { _ = listener.Close() }() stop := context.AfterFunc(ctx, func() { shCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Duration(server.shutdownTimeout)*time.Second) defer cancel() _ = server.gliderServer.Shutdown(shCtx) _ = listener.Close() }) defer stop() err = server.gliderServer.Serve(listener) if err != nil { if errors.Is(err, gliderssh.ErrServerClosed) || ctx.Err() != nil { return nil } return fmt.Errorf("serve SSH: %w", err) } panic("unreachable") } func handle(session gliderssh.Session) { panic("SSH server handler not implemented yet") }
package web type Config struct { Net string `scfg:"net"` Addr string `scfg:"addr"` Root string `scfg:"root"` CookieExpiry int `scfg:"cookie_expiry"` ReadTimeout uint32 `scfg:"read_timeout"` WriteTimeout uint32 `scfg:"write_timeout"` IdleTimeout uint32 `scfg:"idle_timeout"` MaxHeaderBytes int `scfg:"max_header_bytes"` ReverseProxy bool `scfg:"reverse_proxy"` ShutdownTimeout uint32 `scfg:"shutdown_timeout"` }
package web import ( "context" "errors" "fmt" "net" "net/http" "time" "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" ) type Server struct { net string addr string root string httpServer *http.Server shutdownTimeout uint32 }
type Config struct { Net string `scfg:"net"` Addr string `scfg:"addr"` Root string `scfg:"root"` CookieExpiry int `scfg:"cookie_expiry"` ReadTimeout uint32 `scfg:"read_timeout"` WriteTimeout uint32 `scfg:"write_timeout"` IdleTimeout uint32 `scfg:"idle_timeout"` MaxHeaderBytes int `scfg:"max_header_bytes"` ReverseProxy bool `scfg:"reverse_proxy"` ShutdownTimeout uint32 `scfg:"shutdown_timeout"` }
func New(config Config) (server *Server) { httpServer := &http.Server{ Handler: NewHandler(config), ReadTimeout: time.Duration(config.ReadTimeout) * time.Second, WriteTimeout: time.Duration(config.WriteTimeout) * time.Second, IdleTimeout: time.Duration(config.IdleTimeout) * time.Second, MaxHeaderBytes: config.MaxHeaderBytes, } //exhaustruct:ignore return &Server{ net: config.Net, addr: config.Addr, root: config.Root, shutdownTimeout: config.ShutdownTimeout, httpServer: httpServer, } } func (server *Server) Run(ctx context.Context) (err error) { server.httpServer.BaseContext = func(_ net.Listener) context.Context { return ctx } listener, err := misc.Listen(ctx, server.net, server.addr) if err != nil { return fmt.Errorf("listen for web: %w", err) } defer func() { _ = listener.Close() }() stop := context.AfterFunc(ctx, func() { shCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Duration(server.shutdownTimeout)*time.Second) defer cancel() _ = server.httpServer.Shutdown(shCtx) _ = listener.Close() }) defer stop() err = server.httpServer.Serve(listener) if err != nil { if errors.Is(err, http.ErrServerClosed) || ctx.Err() != nil { return nil } return fmt.Errorf("serve web: %w", err) } panic("unreachable") }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package irc import ( "context" "crypto/tls" "fmt" "log/slog" "net" "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" )
// Config contains IRC connection and identity settings for the bot. // This should usually be a part of the primary config struct. 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"` }
// Bot represents an IRC bot client that handles events and allows for sending messages. type Bot struct { // TODO: Use each config field instead of embedding Config here. config *Config ircSendBuffered chan string ircSendDirectChan chan misc.ErrorBack[string] } // NewBot creates a new Bot instance using the provided configuration. func NewBot(c *Config) (b *Bot) { b = &Bot{ config: c, } //exhaustruct:ignore return } // Connect establishes a new IRC session and starts handling incoming and outgoing messages. // This method blocks until an error occurs or the connection is closed. func (b *Bot) Connect(ctx context.Context) error { var err error var underlyingConn net.Conn if b.config.TLS { dialer := tls.Dialer{} //exhaustruct:ignore underlyingConn, err = dialer.DialContext(ctx, b.config.Net, b.config.Addr) } else { dialer := net.Dialer{} //exhaustruct:ignore underlyingConn, err = dialer.DialContext(ctx, b.config.Net, b.config.Addr) } if err != nil { return fmt.Errorf("dialing irc: %w", err) } defer func() { _ = underlyingConn.Close() }() conn := 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.(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 } // Send queues a message to be sent asynchronously via the buffered send queue. // If the queue is full, the message is dropped and an error is logged. func (b *Bot) Send(line string) { select { case b.ircSendBuffered <- line: default: slog.Error("irc sendq full", "line", line) } } // ConnectLoop continuously attempts to maintain an IRC session. // If the connection drops, it automatically retries with no delay. func (b *Bot) ConnectLoop(ctx context.Context) { b.ircSendBuffered = make(chan string, b.config.SendQ) b.ircSendDirectChan = make(chan misc.ErrorBack[string]) for { err := b.Connect(ctx) slog.Error("irc session error", "error", err) } }
package irc // Config contains IRC connection and identity settings for the bot. // This should usually be a part of the primary config struct. 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"` }