diff options
| -rw-r--r-- | .github/workflows/docker.yaml | 1 | ||||
| -rw-r--r-- | .golangci.yml | 1 | ||||
| -rw-r--r-- | README.md | 185 | ||||
| -rw-r--r-- | docker-compose-postgres.yaml | 102 | ||||
| -rw-r--r-- | go.mod | 4 | ||||
| -rw-r--r-- | go.sum | 10 | ||||
| -rw-r--r-- | main.go | 216 | 
7 files changed, 498 insertions, 21 deletions
| diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 71b2988..33354ce 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -35,6 +35,7 @@ jobs:            context: .            file: ./Dockerfile            push: true +          sbom: true            tags: ${{ steps.meta.outputs.tags }}            labels: ${{ steps.meta.outputs.labels }}            provenance: mode=max diff --git a/.golangci.yml b/.golangci.yml index 8a79a07..42f9a39 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -17,3 +17,4 @@ linters-settings:          - github.com/lrstanley/girc          - github.com/sashabaranov/go-openai          - github.com/BurntSushi/toml +        - github.com/jackc/pgx/v5/pgxpool @@ -1,6 +1,6 @@  # milla -Milla is an IRC bot that sends things over to an LLM when you ask it questions and prints the answer with optional syntax-hilighting.<br/> +Milla is an IRC bot that sends things over to an LLM when you ask it questions and prints the answer with optional syntax-highlighting.<br/>  Currently Supported:  - Ollama @@ -39,7 +39,7 @@ The SASL username.  #### ircSaslPass -The SASL password for SASL plain authentication. +The SASL password for SASL plain authentication. Can also be passed as and environment variable.  #### ollamaEndpoint @@ -77,7 +77,7 @@ Which LLM provider to use. The supported options are:  #### apikey -The apikey to use for the LLM provider. +The apikey to use for the LLM provider. Can also be passed as and environment variable.  #### ollamaSystem @@ -89,7 +89,7 @@ The path to the client certificate to use for client cert authentication.  #### serverPass -The password to use for the IRC server the bot is trying to connect to if the server has a password. +The password to use for the IRC server the bot is trying to connect to if the server has a password. Can also be passed as and environment variable.  #### bind @@ -169,6 +169,38 @@ List of channels for the bot to join when it connects to the server.  ircChannels = ["#channel1", "#channel2"]  ``` +### databaseUser + +Name of the database user. Can also be passed an an environment variable. + +### databasePassword + +Password for the database user. Can also be passed an an environment variable. + +### databaseAddress + +Address of the database. Can also be passed as and environment variable. + +### databaseName + +Name of the database. Can also be passed as and environment variable. + +### ircProxy + +Determines which proxy to use to connect to the irc network: + +``` +ircProxy = "socks5://127.0.0.1:9050" +``` + +### llmProxy + +Determines which proxy to use to connect to the LLM endpoint: + +``` +llmProxy = "socks5://127.0.0.1:9050" +``` +  ## Commands  #### help @@ -187,6 +219,20 @@ Get the value of all config options.  Set a config option on the fly. Use the same name as the config file but capitalized. +#### memstats + +Returns memory stats for milla. + +## Environment Variables + +- MILLA_SASL_PASSWORD +- MILLA_SERVER_PASSWORD +- MILLA_APIKEY +- MILLA_DB_USER +- MILLA_DB_PASSWORD +- MILLA_DB_ADDRESS +- MILLA_DB_NAME +  ## Proxy Support  milla will read and use the `ALL_PROXY` environment variable. @@ -200,6 +246,9 @@ ALL_PROXY=127.0.0.1:9050  ## Deploy +### Docker + +Images are automatically pushed to dockerhub. So you can get it from [there](https://hub.docker.com/r/terminaldweller/milla).  An example docker compose file is provided in the repo under `docker-compose.yaml`.  milla can be used with [gvisor](https://gvisor.dev/)'s docker runtime, `runsc`. @@ -234,15 +283,141 @@ networks:      driver: bridge  ``` +### Public Message Storage + +milla can be configured to store all incoming public messages for future use in a postgres database. An example docker compose file is provided under `docker-compose-postgres.yaml`.<br/> + +```yaml +services: +  terra: +    image: milla_distroless_vendored +    build: +      context: . +      dockerfile: ./Dockerfile_distroless_vendored +    deploy: +      resources: +        limits: +          memory: 128M +    logging: +      driver: "json-file" +      options: +        max-size: "100m" +    networks: +      - terranet +    user: 1000:1000 +    restart: unless-stopped +    entrypoint: ["/usr/bin/milla"] +    command: ["--config", "/config.toml"] +    volumes: +      - ./config-gpt.toml:/config.toml +      - /etc/localtime:/etc/localtime:ro +    cap_drop: +      - ALL +    environment: +      - HTTPS_PROXY=http://172.17.0.1:8120 +      - https_proxy=http://172.17.0.1:8120 +      - HTTP_PROXY=http://172.17.0.1:8120 +      - http_proxy=http://172.17.0.1:8120 +  postgres: +    image: postgres:16-alpine3.19 +    deploy: +      resources: +        limits: +          memory: 4096M +    logging: +      driver: "json-file" +      options: +        max-size: "200m" +    restart: unless-stopped +    ports: +      - "127.0.0.1:5455:5432/tcp" +    volumes: +      - terra_postgres_vault:/var/lib/postgresql/data +      - ./scripts/:/docker-entrypoint-initdb.d/:ro +    environment: +      - POSTGRES_PASSWORD_FILE=/run/secrets/pg_pass_secret +      - POSTGRES_USER_FILE=/run/secrets/pg_user_secret +      - POSTGRES_INITDB_ARGS_FILE=/run/secrets/pg_initdb_args_secret +      - POSTGRES_DB_FILE=/run/secrets/pg_db_secret +    networks: +      - terranet +      - dbnet +    secrets: +      - pg_pass_secret +      - pg_user_secret +      - pg_initdb_args_secret +      - pg_db_secret +    runtime: runsc +  pgadmin: +    image: dpage/pgadmin4:8.6 +    deploy: +      resources: +        limits: +          memory: 1024M +    logging: +      driver: "json-file" +      options: +        max-size: "100m" +    environment: +      - PGADMIN_LISTEN_PORT=${PGADMIN_LISTEN_PORT:-5050} +      - PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL:-devi@terminaldweller.com} +      - PGADMIN_DEFAULT_PASSWORD_FILE=/run/secrets/pgadmin_pass +      - PGADMIN_DISABLE_POSTFIX=${PGADMIN_DISABLE_POSTFIX:-YES} +    ports: +      - "127.0.0.1:5050:5050/tcp" +    restart: unless-stopped +    volumes: +      - terra_pgadmin_vault:/var/lib/pgadmin +    networks: +      - dbnet +    secrets: +      - pgadmin_pass +networks: +  terranet: +    driver: bridge +  dbnet: +volumes: +  terra_postgres_vault: +  terra_pgadmin_vault: +secrets: +  pg_pass_secret: +    file: ./pg/pg_pass_secret +  pg_user_secret: +    file: ./pg/pg_user_secret +  pg_initdb_args_secret: +    file: ./pg/pg_initdb_args_secret +  pg_db_secret: +    file: ./pg/pg_db_secret +  pgadmin_pass: +    file: ./pgadmin/pgadmin_pass +``` +  The env vars `UID`and `GID`need to be defined or they can replaces by your host user's uid and gid.<br/> -As a convinience, there is a a [distroless](https://github.com/GoogleContainerTools/distroless) dockerfile, `Dockerfile_distroless` also provided.<br/> +As a convenience, there is a a [distroless](https://github.com/GoogleContainerTools/distroless) dockerfile, `Dockerfile_distroless` also provided.<br/>  A vendored build of milla is available by first running `go mod vendor` and then using the provided Dockerfile, `Dockerfile_distroless_vendored`.<br/> +### Build + +For a regular build: + +```sh +go mod download +go build +``` + +For a vendored build: + +```sh +go mod vendor +go build +``` +  ## Thanks  - [girc](https://github.com/lrstanley/girc)  - [chroma](https://github.com/alecthomas/chroma) +- [pgx](https://github.com/jackc/pgx)  - [ollama](https://github.com/ollama/ollama)  ## Similar Projects diff --git a/docker-compose-postgres.yaml b/docker-compose-postgres.yaml new file mode 100644 index 0000000..a10e79f --- /dev/null +++ b/docker-compose-postgres.yaml @@ -0,0 +1,102 @@ +services: +  terra: +    image: milla_distroless_vendored +    build: +      context: . +      dockerfile: ./Dockerfile_distroless_vendored +    deploy: +      resources: +        limits: +          memory: 128M +    logging: +      driver: "json-file" +      options: +        max-size: "100m" +    networks: +      - terranet +    user: 1000:1000 +    restart: unless-stopped +    entrypoint: ["/usr/bin/milla"] +    command: ["--config", "/config.toml"] +    volumes: +      - ./config-gpt.toml:/config.toml +      - /etc/localtime:/etc/localtime:ro +    cap_drop: +      - ALL +    environment: +      - HTTPS_PROXY=http://172.17.0.1:8120 +      - https_proxy=http://172.17.0.1:8120 +      - HTTP_PROXY=http://172.17.0.1:8120 +      - http_proxy=http://172.17.0.1:8120 +  postgres: +    image: postgres:16-alpine3.19 +    deploy: +      resources: +        limits: +          memory: 4096M +    logging: +      driver: "json-file" +      options: +        max-size: "200m" +    restart: unless-stopped +    ports: +      - "127.0.0.1:5455:5432/tcp" +    volumes: +      - terra_postgres_vault:/var/lib/postgresql/data +      - ./scripts/:/docker-entrypoint-initdb.d/:ro +    environment: +      - POSTGRES_PASSWORD_FILE=/run/secrets/pg_pass_secret +      - POSTGRES_USER_FILE=/run/secrets/pg_user_secret +      - POSTGRES_INITDB_ARGS_FILE=/run/secrets/pg_initdb_args_secret +      - POSTGRES_DB_FILE=/run/secrets/pg_db_secret +    networks: +      - terranet +      - dbnet +    secrets: +      - pg_pass_secret +      - pg_user_secret +      - pg_initdb_args_secret +      - pg_db_secret +    runtime: runsc +  pgadmin: +    image: dpage/pgadmin4:8.6 +    deploy: +      resources: +        limits: +          memory: 1024M +    logging: +      driver: "json-file" +      options: +        max-size: "100m" +    environment: +      - PGADMIN_LISTEN_PORT=${PGADMIN_LISTEN_PORT:-5050} +      - PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL:-devi@terminaldweller.com} +      - PGADMIN_DEFAULT_PASSWORD_FILE=/run/secrets/pgadmin_pass +      - PGADMIN_DISABLE_POSTFIX=${PGADMIN_DISABLE_POSTFIX:-YES} +    ports: +      - "127.0.0.1:5050:5050/tcp" +    restart: unless-stopped +    volumes: +      - terra_pgadmin_vault:/var/lib/pgadmin +    networks: +      - dbnet +    secrets: +      - pgadmin_pass +networks: +  terranet: +    driver: bridge +  dbnet: +volumes: +  terra_postgres_vault: +  terra_pgadmin_vault: +secrets: +  pg_pass_secret: +    file: ./pg/pg_pass_secret +  pg_user_secret: +    file: ./pg/pg_user_secret +  pg_initdb_args_secret: +    file: ./pg/pg_initdb_args_secret +  pg_db_secret: +    file: ./pg/pg_db_secret +  pgadmin_pass: +    file: ./pgadmin/pgadmin_pass @@ -6,6 +6,7 @@ require (  	github.com/BurntSushi/toml v0.3.1  	github.com/alecthomas/chroma/v2 v2.12.0  	github.com/google/generative-ai-go v0.11.2 +	github.com/jackc/pgx/v5 v5.5.5  	github.com/lrstanley/girc v0.0.0-20240125042120-9add3166e52e  	github.com/sashabaranov/go-openai v1.19.3  	golang.org/x/net v0.24.0 @@ -29,6 +30,9 @@ require (  	github.com/google/uuid v1.6.0 // indirect  	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect  	github.com/googleapis/gax-go/v2 v2.12.4 // indirect +	github.com/jackc/pgpassfile v1.0.0 // indirect +	github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect +	github.com/jackc/puddle/v2 v2.2.1 // indirect  	go.opencensus.io v0.24.0 // indirect  	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect  	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect @@ -75,6 +75,14 @@ github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw  github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=  github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=  github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=  github.com/lrstanley/girc v0.0.0-20240125042120-9add3166e52e h1:Y86mAFtJjS4P0atZ6QAKH88TV0ASQYJdIGWiOmJKoNY=  github.com/lrstanley/girc v0.0.0-20240125042120-9add3166e52e/go.mod h1:lgrnhcF8bg/Bd5HA5DOb4Z+uGqUqGnp4skr+J2GwVgI=  github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -85,6 +93,8 @@ github.com/sashabaranov/go-openai v1.19.3/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adO  github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=  github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=  github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=  github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=  github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=  github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -9,10 +9,13 @@ import (  	"flag"  	"fmt"  	"log" +	"net"  	"net/http" +	"net/url"  	"os"  	"reflect"  	"regexp" +	"runtime"  	"strconv"  	"strings"  	"time" @@ -20,6 +23,7 @@ import (  	"github.com/BurntSushi/toml"  	"github.com/alecthomas/chroma/v2/quick"  	"github.com/google/generative-ai-go/genai" +	"github.com/jackc/pgx/v5/pgxpool"  	"github.com/lrstanley/girc"  	openai "github.com/sashabaranov/go-openai"  	"golang.org/x/net/proxy" @@ -33,6 +37,7 @@ var (  	errCantSet           = errors.New("can't set field")  	errWrongDataForField = errors.New("wrong data type for field")  	errUnsupportedType   = errors.New("unsupported type") +	dbConnection         *pgxpool.Pool //nolint:gochecknoglobals  )  type TomlConfig struct { @@ -50,6 +55,13 @@ type TomlConfig struct {  	ClientCertPath      string   `toml:"clientCertPath"`  	ServerPass          string   `toml:"serverPass"`  	Bind                string   `toml:"bind"` +	Name                string   `toml:"name"` +	DatabaseAddress     string   `toml:"databaseAddress"` +	DatabasePassword    string   `toml:"databasePassword"` +	DatabaseUser        string   `toml:"databaseUser"` +	DatabaseName        string   `toml:"databaseName"` +	LLMProxy            string   `toml:"llmProxy"` +	IRCProxy            string   `toml:"ircProxy"`  	Temp                float64  `toml:"temp"`  	RequestTimeout      int      `toml:"requestTimeout"`  	MillaReconnectDelay int      `toml:"millaReconnectDelay"` @@ -69,6 +81,7 @@ type TomlConfig struct {  	Out                 bool     `toml:"out"`  	Admins              []string `toml:"admins"`  	IrcChannels         []string `toml:"ircChannels"` +	ScrapeChannels      []string `toml:"scrapeChannels"`  }  func NewTomlConfig() *TomlConfig { @@ -78,6 +91,9 @@ func NewTomlConfig() *TomlConfig {  		ChromaStyle:         "rose-pine-moon",  		ChromaFormatter:     "noop",  		Provider:            "ollama", +		DatabaseAddress:     "postgres", +		DatabaseUser:        "milla", +		DatabaseName:        "milladb",  		Temp:                0.5,  //nolint:gomnd  		RequestTimeout:      10,   //nolint:gomnd  		MillaReconnectDelay: 30,   //nolint:gomnd @@ -206,6 +222,7 @@ func getHelpString() string {  	helpString += "set - set a configuration value\n"  	helpString += "get - get a configuration value\n"  	helpString += "getall - returns all config options with their value\n" +	helpString += "memstats - returns the memory status currently being used\n"  	return helpString  } @@ -251,6 +268,11 @@ func setFieldByName(v reflect.Value, field string, value string) error {  	return nil  } +func byteToMByte(bytes uint64, +) uint64 { +	return bytes / 1024 / 1024 +} +  func runCommand(  	client *girc.Client,  	event girc.Event, @@ -317,6 +339,13 @@ func runCommand(  			fieldValue := v.Field(i).Interface()  			client.Cmd.Reply(event, fmt.Sprintf("%s: %v", field.Name, fieldValue))  		} +	case "memstats": +		var memStats runtime.MemStats +		runtime.ReadMemStats(&memStats) + +		client.Cmd.Reply(event, fmt.Sprintf("Alloc: %d MiB", byteToMByte(memStats.Alloc))) +		client.Cmd.Reply(event, fmt.Sprintf("TotalAlloc: %d MiB", byteToMByte(memStats.TotalAlloc))) +		client.Cmd.Reply(event, fmt.Sprintf("Sys: %d MiB", byteToMByte(memStats.Sys)))  	default:  		client.Cmd.Reply(event, errUnknCmd.Error())  	} @@ -385,12 +414,28 @@ func ollamaHandler(  		var httpClient http.Client -		dialer := proxy.FromEnvironment() +		var dialer proxy.Dialer -		httpClient = http.Client{ -			Transport: &http.Transport{ -				Dial: dialer.Dial, -			}, +		if appConfig.LLMProxy != "" { +			proxyURL, err := url.Parse(appConfig.IRCProxy) +			if err != nil { +				cancel() + +				log.Fatal(err.Error()) +			} + +			dialer, err = proxy.FromURL(proxyURL, &net.Dialer{Timeout: time.Duration(appConfig.RequestTimeout) * time.Second}) +			if err != nil { +				cancel() + +				log.Fatal(err.Error()) +			} + +			httpClient = http.Client{ +				Transport: &http.Transport{ +					Dial: dialer.Dial, +				}, +			}  		}  		response, err := httpClient.Do(request) @@ -474,6 +519,10 @@ func geminiHandler(  		// clientGemini, err := genai.NewClient(ctx, option.WithAPIKey(appConfig.Apikey), option.WithHTTPClient(&httpClient)) +		if appConfig.Apikey == "" { +			appConfig.Apikey = os.Getenv("MILLA_APIKEY") +		} +  		clientGemini, err := genai.NewClient(ctx, option.WithAPIKey(appConfig.Apikey))  		if err != nil {  			client.Cmd.ReplyTo(event, fmt.Sprintf("error: %s", err.Error())) @@ -559,12 +608,30 @@ func chatGPTHandler(  		var httpClient http.Client -		dialer := proxy.FromEnvironment() +		if appConfig.LLMProxy != "" { +			proxyURL, err := url.Parse(appConfig.IRCProxy) +			if err != nil { +				cancel() -		httpClient = http.Client{ -			Transport: &http.Transport{ -				Dial: dialer.Dial, -			}, +				log.Fatal(err.Error()) +			} + +			dialer, err := proxy.FromURL(proxyURL, &net.Dialer{Timeout: time.Duration(appConfig.RequestTimeout) * time.Second}) +			if err != nil { +				cancel() + +				log.Fatal(err.Error()) +			} + +			httpClient = http.Client{ +				Transport: &http.Transport{ +					Dial: dialer.Dial, +				}, +			} +		} + +		if appConfig.Apikey == "" { +			appConfig.Apikey = os.Getenv("MILLA_APIKEY")  		}  		config := openai.DefaultConfig(appConfig.Apikey) @@ -613,7 +680,78 @@ func chatGPTHandler(  	})  } -func runIRC(appConfig TomlConfig, ircChan chan *girc.Client) { +func connectToDB(appConfig TomlConfig, context *context.Context) { +	for { +		if appConfig.DatabaseUser == "" { +			appConfig.DatabaseUser = os.Getenv("MILLA_DB_USER") +		} + +		if appConfig.DatabasePassword == "" { +			appConfig.DatabasePassword = os.Getenv("MILLA_DB_PASSWORD") +		} + +		if appConfig.DatabaseAddress == "" { +			appConfig.DatabaseAddress = os.Getenv("MILLA_DB_ADDRESS") +		} + +		if appConfig.DatabaseName == "" { +			appConfig.DatabaseName = os.Getenv("MILLA_DB_NAME") +		} + +		dbURL := fmt.Sprintf( +			"postgres://%s:%s@%s/%s", +			appConfig.DatabaseUser, +			appConfig.DatabasePassword, +			appConfig.DatabaseAddress, +			appConfig.DatabaseName) + +		conn, err := pgxpool.New(*context, dbURL) +		if err != nil { +			log.Println(err) +			time.Sleep(time.Duration(appConfig.MillaReconnectDelay) * time.Second) +		} else { +			for _, channel := range appConfig.ScrapeChannels { +				query := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id SERIAL PRIMARY KEY,channel TEXT NOT NULL,log TEXT NOT NULL,nick TEXT NOT NULL,dateadded TIMESTAMP DEFAULT CURRENT_TIMESTAMP)", +					strings.ReplaceAll(channel, "#", "")) + +				log.Println(query) + +				_, err = conn.Query(*context, query) +				if err != nil { +					log.Println(err.Error()) +					time.Sleep(time.Duration(appConfig.MillaReconnectDelay) * time.Second) +				} +			} + +			dbConnection = conn +		} +	} +} + +func scrapeChannel(irc *girc.Client) { +	irc.Handlers.AddBg(girc.PRIVMSG, func(client *girc.Client, event girc.Event) { +		if dbConnection == nil { +			log.Println("missed logging message because currently not connected to db") + +			return +		} +		query := fmt.Sprintf("INSERT INTO %s (channel,log,nick) VALUES ('%s','%s','%s')", +			strings.ReplaceAll(event.Params[0], "#", ""), +			event.Params[0], +			event.Last(), +			event.Source.Name, +		) +		log.Println(query) + +		_, err := dbConnection.Query( +			context.Background(), query) +		if err != nil { +			log.Println(err.Error()) +		} +	}) +} + +func runIRC(appConfig TomlConfig, ircChan chan *girc.Client, dbChan chan *pgxpool.Pool) {  	var OllamaMemory []MemoryElement  	var GeminiMemory []*genai.Content @@ -646,16 +784,29 @@ func runIRC(appConfig TomlConfig, ircChan chan *girc.Client) {  		irc.Config.Out = os.Stdout  	} -	if appConfig.ServerPass != "" { -		irc.Config.ServerPass = appConfig.ServerPass +	if appConfig.ServerPass == "" { +		appConfig.ServerPass = os.Getenv("MILLA_SERVER_PASSWORD")  	} +	irc.Config.ServerPass = appConfig.ServerPass +  	if appConfig.Bind != "" {  		irc.Config.Bind = appConfig.Bind  	} +	if appConfig.Name != "" { +		irc.Config.Name = appConfig.Name +	} +  	saslUser := appConfig.IrcSaslUser -	saslPass := appConfig.IrcSaslPass + +	var saslPass string + +	if appConfig.IrcSaslPass == "" { +		saslPass = os.Getenv("MILLA_SASL_PASSWORD") +	} else { +		saslPass = appConfig.IrcSaslPass +	}  	if appConfig.EnableSasl && saslUser != "" && saslPass != "" {  		irc.Config.SASL = &girc.SASLPlain{ @@ -690,10 +841,42 @@ func runIRC(appConfig TomlConfig, ircChan chan *girc.Client) {  		chatGPTHandler(irc, &appConfig, &GPTMemory)  	} +	context, cancel := context.WithTimeout(context.Background(), time.Duration(appConfig.RequestTimeout)*time.Second) +	defer cancel() + +	go connectToDB(appConfig, &context) + +	if len(appConfig.ScrapeChannels) > 0 { +		irc.Handlers.AddBg(girc.CONNECTED, func(c *girc.Client, e girc.Event) { +			for _, channel := range appConfig.ScrapeChannels { +				c.Cmd.Join(channel) +			} +		}) + +		go scrapeChannel(irc) +	}  	ircChan <- irc  	for { -		if err := irc.Connect(); err != nil { +		var dialer proxy.Dialer + +		if appConfig.IRCProxy != "" { +			proxyURL, err := url.Parse(appConfig.IRCProxy) +			if err != nil { +				cancel() + +				log.Fatal(err.Error()) +			} + +			dialer, err = proxy.FromURL(proxyURL, &net.Dialer{Timeout: time.Duration(appConfig.RequestTimeout) * time.Second}) +			if err != nil { +				cancel() + +				log.Fatal(err.Error()) +			} +		} + +		if err := irc.DialerConnect(dialer); err != nil {  			log.Println(err)  			log.Println("reconnecting in " + strconv.Itoa(appConfig.MillaReconnectDelay))  			time.Sleep(time.Duration(appConfig.MillaReconnectDelay) * time.Second) @@ -723,6 +906,7 @@ func main() {  	log.Println(appConfig)  	ircChan := make(chan *girc.Client, 1) +	dbConn := make(chan *pgxpool.Pool, 1) -	runIRC(*appConfig, ircChan) +	runIRC(*appConfig, ircChan, dbConn)  } | 
