Hi… I am well aware that this diff view is very suboptimal. It will be fixed when the refactored server comes along!
Lint ipc
version: "2"
linters:
default: all
disable:
- depguard
- wsl_v5 # tmp
- wsl # tmp
- unused # tmp
- nonamedreturns
- err113 # tmp
- gochecknoinits # tmp
- nlreturn # tmp
- cyclop # tmp
- gocognit # tmp
- varnamelen # tmp
- funlen # tmp
- lll
- mnd # tmp
- revive # tmp
- godox # tmp
- nestif # tmp
linters-settings:
revive:
rules:
- name: error-strings
disabled: true
issues:
max-issues-per-linter: 0
max-same-issues: 0
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package git2c import (
"context"
"fmt"
"net"
"go.lindenii.runxiyu.org/forge/forged/internal/common/bare"
)
// Client represents a connection to the git2d backend daemon.
type Client struct {
socketPath string
conn net.Conn
writer *bare.Writer
reader *bare.Reader
}
// NewClient establishes a connection to a git2d socket and returns a new Client.
func NewClient(socketPath string) (*Client, error) {
conn, err := net.Dial("unix", socketPath)
func NewClient(ctx context.Context, socketPath string) (*Client, error) {
dialer := &net.Dialer{} //exhaustruct:ignore
conn, err := dialer.DialContext(ctx, "unix", socketPath)
if err != nil {
return nil, fmt.Errorf("git2d connection failed: %w", err)
}
writer := bare.NewWriter(conn)
reader := bare.NewReader(conn)
return &Client{
socketPath: socketPath,
conn: conn,
writer: writer,
reader: reader,
}, nil
}
// Close terminates the underlying socket connection.
func (c *Client) Close() error {
func (c *Client) Close() (err error) {
if c.conn != nil {
return c.conn.Close()
err = c.conn.Close()
if err != nil {
return fmt.Errorf("close underlying socket: %w", err)
}
} return nil }
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package git2c
import (
"encoding/hex"
"errors"
"fmt"
"io"
)
// CmdIndex requests a repository index from git2d and returns the list of commits
// and the contents of a README file if available.
func (c *Client) CmdIndex(repoPath string) ([]Commit, *FilenameContents, error) {
if err := c.writer.WriteData([]byte(repoPath)); err != nil {
err := c.writer.WriteData([]byte(repoPath))
if err != nil {
return nil, nil, fmt.Errorf("sending repo path failed: %w", err)
}
if err := c.writer.WriteUint(1); err != nil {
err = c.writer.WriteUint(1)
if err != nil {
return nil, nil, fmt.Errorf("sending command failed: %w", err)
}
status, err := c.reader.ReadUint()
if err != nil {
return nil, nil, fmt.Errorf("reading status failed: %w", err)
}
if status != 0 {
return nil, nil, fmt.Errorf("git2d error: %d", status)
}
// README
readmeRaw, err := c.reader.ReadData()
if err != nil {
readmeRaw = nil
}
readmeFilename := "README.md" // TODO
readme := &FilenameContents{Filename: readmeFilename, Content: readmeRaw}
// Commits
var commits []Commit
for {
id, err := c.reader.ReadData()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, nil, fmt.Errorf("reading commit ID failed: %w", err)
}
title, _ := c.reader.ReadData()
authorName, _ := c.reader.ReadData()
authorEmail, _ := c.reader.ReadData()
authorDate, _ := c.reader.ReadData()
commits = append(commits, Commit{
Hash: hex.EncodeToString(id),
Author: string(authorName),
Email: string(authorEmail),
Date: string(authorDate),
Message: string(title),
})
}
return commits, readme, nil
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package git2c
import (
"errors"
"fmt"
"io"
)
// CmdTreeRaw queries git2d for a tree or blob object at the given path within the repository.
// It returns either a directory listing or the contents of a file.
func (c *Client) CmdTreeRaw(repoPath, pathSpec string) ([]TreeEntry, string, error) {
if err := c.writer.WriteData([]byte(repoPath)); err != nil {
err := c.writer.WriteData([]byte(repoPath))
if err != nil {
return nil, "", fmt.Errorf("sending repo path failed: %w", err)
}
if err := c.writer.WriteUint(2); err != nil {
err = c.writer.WriteUint(2)
if err != nil {
return nil, "", fmt.Errorf("sending command failed: %w", err)
}
if err := c.writer.WriteData([]byte(pathSpec)); err != nil {
err = c.writer.WriteData([]byte(pathSpec))
if err != nil {
return nil, "", fmt.Errorf("sending path failed: %w", err)
}
status, err := c.reader.ReadUint()
if err != nil {
return nil, "", fmt.Errorf("reading status failed: %w", err)
}
switch status {
case 0:
kind, err := c.reader.ReadUint()
if err != nil {
return nil, "", fmt.Errorf("reading object kind failed: %w", err)
}
switch kind {
case 1:
// Tree
count, err := c.reader.ReadUint()
if err != nil {
return nil, "", fmt.Errorf("reading entry count failed: %w", err)
}
var files []TreeEntry
for range count {
typeCode, err := c.reader.ReadUint()
if err != nil {
return nil, "", fmt.Errorf("error reading entry type: %w", err)
}
mode, err := c.reader.ReadUint()
if err != nil {
return nil, "", fmt.Errorf("error reading entry mode: %w", err)
}
size, err := c.reader.ReadUint()
if err != nil {
return nil, "", fmt.Errorf("error reading entry size: %w", err)
}
name, err := c.reader.ReadData()
if err != nil {
return nil, "", fmt.Errorf("error reading entry name: %w", err)
}
files = append(files, TreeEntry{
Name: string(name),
Mode: fmt.Sprintf("%06o", mode),
Size: size,
IsFile: typeCode == 2,
IsSubtree: typeCode == 1,
})
}
return files, "", nil
case 2:
// Blob
content, err := c.reader.ReadData()
if err != nil && !errors.Is(err, io.EOF) {
return nil, "", fmt.Errorf("error reading file content: %w", err)
}
return nil, string(content), nil
default:
return nil, "", fmt.Errorf("unknown kind: %d", kind)
}
case 3:
return nil, "", fmt.Errorf("path not found: %s", pathSpec)
default:
return nil, "", fmt.Errorf("unknown status code: %d", status)
}
}
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> // TODO: Make the C part report detailed error messages too package git2c import "errors" var (
Success error
ErrUnknown = errors.New("git2c: unknown error")
ErrPath = errors.New("git2c: get tree entry by path failed")
ErrRevparse = errors.New("git2c: revparse failed")
ErrReadme = errors.New("git2c: no readme")
ErrBlobExpected = errors.New("git2c: blob expected")
ErrEntryToObject = errors.New("git2c: tree entry to object conversion failed")
ErrBlobRawContent = errors.New("git2c: get blob raw content failed")
ErrRevwalk = errors.New("git2c: revwalk failed")
ErrRevwalkPushHead = errors.New("git2c: revwalk push head failed")
ErrBareProto = errors.New("git2c: bare protocol error")
)
func Perror(errno uint) error {
switch errno {
case 0:
return Success
return nil
case 3: return ErrPath case 4: return ErrRevparse case 5: return ErrReadme case 6: return ErrBlobExpected case 7: return ErrEntryToObject case 8: return ErrBlobRawContent case 9: return ErrRevwalk case 10: return ErrRevwalkPushHead case 11: return ErrBareProto } return ErrUnknown }
// 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() error {
func (b *Bot) Connect(ctx context.Context) error {
var err error
var underlyingConn net.Conn
if b.config.TLS {
underlyingConn, err = tls.Dial(b.config.Net, b.config.Addr, nil)
dialer := tls.Dialer{} //exhaustruct:ignore
underlyingConn, err = dialer.DialContext(ctx, b.config.Net, b.config.Addr)
} else {
underlyingConn, err = net.Dial(b.config.Net, b.config.Addr)
dialer := net.Dialer{} //exhaustruct:ignore
underlyingConn, err = dialer.DialContext(ctx, b.config.Net, b.config.Addr)
}
if err != nil {
return err
return fmt.Errorf("dialing irc: %w", err)
}
defer underlyingConn.Close()
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() {
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()
err := b.Connect(ctx)
slog.Error("irc session error", "error", err)
}
}
package irc import ( "bufio"
"fmt"
"net"
"slices"
"go.lindenii.runxiyu.org/forge/forged/internal/common/misc"
)
type Conn struct {
netConn net.Conn
bufReader *bufio.Reader
}
func NewConn(netConn net.Conn) Conn {
return Conn{
netConn: netConn,
bufReader: bufio.NewReader(netConn),
}
}
func (c *Conn) ReadMessage() (msg Message, line string, err error) {
raw, err := c.bufReader.ReadSlice('\n')
if err != nil {
return
}
if raw[len(raw)-1] == '\n' {
raw = raw[:len(raw)-1]
}
if raw[len(raw)-1] == '\r' {
raw = raw[:len(raw)-1]
}
lineBytes := slices.Clone(raw)
line = misc.BytesToString(lineBytes)
msg, err = Parse(lineBytes)
return
}
func (c *Conn) Write(p []byte) (n int, err error) {
return c.netConn.Write(p)
n, err = c.netConn.Write(p)
if err != nil {
err = fmt.Errorf("write to connection: %w", err)
}
return n, err
}
func (c *Conn) WriteString(s string) (n int, err error) {
return c.netConn.Write(misc.StringToBytes(s))
n, err = c.netConn.Write(misc.StringToBytes(s))
if err != nil {
err = fmt.Errorf("write to connection: %w", err)
}
return n, err
}
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: Copyright (c) 2018-2024 luk3yx <https://luk3yx.github.io>
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package irc
import (
"bytes"
"go.lindenii.runxiyu.org/forge/forged/internal/common/misc"
)
type Message struct {
Command string
Source Source
Tags map[string]string
Args []string
}
// All strings returned are borrowed from the input byte slice.
func Parse(raw []byte) (msg Message, err error) {
sp := bytes.Split(raw, []byte{' '}) // TODO: Use bytes.Cut instead here
if bytes.HasPrefix(sp[0], []byte{'@'}) { // TODO: Check size manually
if len(sp[0]) < 2 {
err = ErrMalformedMsg
return
return msg, err
}
sp[0] = sp[0][1:]
msg.Tags, err = tagsToMap(sp[0])
if err != nil {
return
return msg, err
}
if len(sp) < 2 {
err = ErrMalformedMsg
return
return msg, err
}
sp = sp[1:]
} else {
msg.Tags = nil // TODO: Is a nil map the correct thing to use here?
}
if bytes.HasPrefix(sp[0], []byte{':'}) { // TODO: Check size manually
if len(sp[0]) < 2 {
err = ErrMalformedMsg
return
return msg, err
}
sp[0] = sp[0][1:]
msg.Source = parseSource(sp[0])
if len(sp) < 2 {
err = ErrMalformedMsg
return
return msg, err
}
sp = sp[1:]
}
msg.Command = misc.BytesToString(sp[0])
if len(sp) < 2 {
return
return msg, err
}
sp = sp[1:]
for i := 0; i < len(sp); i++ {
if len(sp[i]) == 0 {
continue
}
if sp[i][0] == ':' {
if len(sp[i]) < 2 {
sp[i] = []byte{}
} else {
sp[i] = sp[i][1:]
}
msg.Args = append(msg.Args, misc.BytesToString(bytes.Join(sp[i:], []byte{' '})))
// TODO: Avoid Join by not using sp in the first place
break
}
msg.Args = append(msg.Args, misc.BytesToString(sp[i]))
}
return
return msg, err
}
var ircv3TagEscapes = map[byte]byte{ //nolint:gochecknoglobals
':': ';',
's': ' ',
'r': '\r',
'n': '\n',
}
func tagsToMap(raw []byte) (tags map[string]string, err error) {
tags = make(map[string]string)
for rawTag := range bytes.SplitSeq(raw, []byte{';'}) {
key, value, found := bytes.Cut(rawTag, []byte{'='})
if !found {
err = ErrInvalidIRCv3Tag
return
return tags, err
}
if len(value) == 0 {
tags[misc.BytesToString(key)] = ""
} else {
if !bytes.Contains(value, []byte{'\\'}) {
tags[misc.BytesToString(key)] = misc.BytesToString(value)
} else {
valueUnescaped := bytes.NewBuffer(make([]byte, 0, len(value)))
for i := 0; i < len(value); i++ {
if value[i] == '\\' {
i++
byteUnescaped, ok := ircv3TagEscapes[value[i]]
if !ok {
byteUnescaped = value[i]
}
valueUnescaped.WriteByte(byteUnescaped)
} else {
valueUnescaped.WriteByte(value[i])
}
}
tags[misc.BytesToString(key)] = misc.BytesToString(valueUnescaped.Bytes())
}
}
}
return
return tags, err
}
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package irc
import (
"bytes"
"go.lindenii.runxiyu.org/forge/forged/internal/common/misc"
)
type Source interface {
AsSourceString() string
}
//nolint:ireturn
func parseSource(s []byte) Source {
nick, userhost, found := bytes.Cut(s, []byte{'!'})
if !found {
return Server{name: misc.BytesToString(s)}
}
user, host, found := bytes.Cut(userhost, []byte{'@'})
if !found {
return Server{name: misc.BytesToString(s)}
}
return Client{
Nick: misc.BytesToString(nick),
User: misc.BytesToString(user),
Host: misc.BytesToString(host),
}
}
type Server struct {
name string
}
func (s Server) AsSourceString() string {
return s.name
}
type Client struct {
Nick string
User string
Host string
}
func (c Client) AsSourceString() string {
return c.Nick + "!" + c.User + "@" + c.Host
}