Lindenii Project Forge
Login

server

Lindenii Forge’s main backend daemon
Commit info
ID
8b7fa11042b8a4ec7ed34fde44ba7f67669c80ba
Author
Runxi Yu <me@runxiyu.org>
Author date
Sun, 06 Apr 2025 02:29:10 +0800
Committer
Runxi Yu <me@runxiyu.org>
Committer date
Sun, 06 Apr 2025 02:29:10 +0800
Actions
Add package descriptions
// Package ansiec provides definitions for ANSI escape sequences.
package ansiec
// Package database provides stubs and wrappers for databases.
package database

import (
	"context"

	"github.com/jackc/pgx/v5/pgxpool"
)

type Database struct {
	*pgxpool.Pool
}

func Open(connString string) (Database, error) {
	db, err := pgxpool.New(context.Background(), connString)
	return Database{db}, err
}
// Package embed provides embedded filesystems created in build-time.
package embed

import "embed"

//go:embed LICENSE* source.tar.gz
var Source embed.FS

//go:embed templates/* static/*
//go:embed hookc/hookc git2d/git2d
var Resources embed.FS
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

// Package git2c provides routines to interact with the git2d backend daemon.
package git2c

import (
	"fmt"
	"net"

	"git.sr.ht/~sircmpwn/go-bare"
)

type Client struct {
	SocketPath string
	conn       net.Conn
	writer     *bare.Writer
	reader     *bare.Reader
}

func NewClient(socketPath string) (*Client, error) {
	conn, err := net.Dial("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
}

func (c *Client) Close() error {
	if c.conn != nil {
		return c.conn.Close()
	}
	return nil
}
// Package irc provides basic IRC bot functionality.
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)
	}
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

// Package misc provides miscellaneous functions and other definitions.
package misc

import "strings"

// sliceContainsNewlines returns true if and only if the given slice contains
// one or more strings that contains newlines.
func SliceContainsNewlines(s []string) bool {
	for _, v := range s {
		if strings.Contains(v, "\n") {
			return true
		}
	}
	return false
}
// Package oldgit provides deprecated functions that depend on go-git.
package oldgit
// Package render provides functions to render code and READMEs.
package render
// Package unsorted is where unsorted Go files from the old structure are kept.
package unsorted
// Package web provides web-facing components of the forge.
package web