diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 44c8afa..0d2cbb5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -38,7 +38,7 @@ jobs:
uses: actions/setup-go@v7
with:
# Keep CI on the patched standard library release reported by govulncheck.
- go-version: '1.26.6'
+ go-version: '1.26.9'
cache: true
- name: Check formatting
@@ -57,6 +57,16 @@ jobs:
- name: Run go vet
run: go vet ./...
+ - name: Locate Chrome for offline browser regressions
+ shell: bash
+ run: |
+ test_browser="$(command -v google-chrome || command -v chromium || command -v chromium-browser || true)"
+ if [[ -z "$test_browser" ]]; then
+ echo "Chrome or Chromium is required for the offline Ask regression tests." >&2
+ exit 1
+ fi
+ printf 'GOBOT_TEST_BROWSER=%s\n' "$test_browser" >> "$GITHUB_ENV"
+
- name: Run tests with race detection
run: go test -race ./...
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index fb73fe1..e2e142b 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -37,18 +37,18 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v7
with:
- go-version: '1.26.6'
+ go-version: '1.26.9'
cache: true
- name: Initialize CodeQL
- uses: github/codeql-action/init@v4.38.2
+ uses: github/codeql-action/init@v4.38.3
with:
languages: go
- name: Autobuild
- uses: github/codeql-action/autobuild@v4.38.2
+ uses: github/codeql-action/autobuild@v4.38.3
- name: Perform CodeQL analysis
- uses: github/codeql-action/analyze@v4.38.2
+ uses: github/codeql-action/analyze@v4.38.3
with:
category: /language:go
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d42864d..f8657af 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -13,7 +13,8 @@ review and maintain.
## Local checks
-GoBot currently targets Go `1.26.6` or newer. Before opening a pull request:
+GoBot targets Go `1.26.9+` (or `1.27.2+` for the Go 1.27 series).
+Before opening a pull request:
```sh
go test ./...
@@ -26,6 +27,16 @@ make build
Use `gofmt` on changed Go files. If a change adds a plugin or configuration
option, include focused tests and update the relevant guide under `docs/`.
+For changes to the rendered `!ask` fallback or its browser dependencies, also
+run the offline browser fixtures with your Chrome/Chromium executable:
+
+```sh
+GOBOT_TEST_BROWSER=/path/to/chrome go test -race ./plugins -run 'TestAskRendered' -count=1
+```
+
+These fixtures use synthetic pages without contacting DuckDuckGo or sending
+IRC messages. CI runs them with the runner's installed browser as well.
+
## Branches and pull requests
- Start from an up-to-date `main` branch.
diff --git a/Dockerfile b/Dockerfile
index 7969e51..f0f6f6a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
-FROM golang:1.26.6-alpine AS builder
+FROM golang:1.26.9-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
diff --git a/README.md b/README.md
index 479a9bd..d5e37e6 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
[](https://github.com/variablenix/GoBot/actions/workflows/ci.yml)
[](https://github.com/variablenix/GoBot/actions/workflows/codeql.yml)
-[](go.mod)
+[](go.mod)
[](LICENSE)
GoBot is an extensible Go IRC bot for long-running use on one or more IRC
@@ -53,7 +53,7 @@ are also persisted so they survive restarts.
Requirements:
-- Go `1.26.6+` or a newer supported release
+- Go `1.26.9+` (or `1.27.2+` when using the Go 1.27 series)
- IRC network access
- a writable data directory
diff --git a/docs/deployment.md b/docs/deployment.md
index c1fd13a..e5333de 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -5,7 +5,7 @@ systemd service, and using Docker.
## Requirements
-- Go `1.26.6+` or a newer supported release
+- Go `1.26.9+` (or `1.27.2+` when using the Go 1.27 series)
- a writable filesystem for the BoltDB database
- network access to the IRC servers
- optionally, Docker and Docker Compose
@@ -42,6 +42,11 @@ make test
Configuration-only changes require a restart. Code changes require a new
build before restarting.
+Use a patched Go toolchain when rebuilding: Go 1.26.9 and 1.27.2 contain
+standard-library security fixes that are not supplied by updating modules
+alone. An existing binary retains its old standard library until it is rebuilt
+and the service is restarted. CI and the Docker builder use Go 1.26.9.
+
## First-run workflow
There is no setup wizard yet. GoBot is configuration-first:
diff --git a/go.mod b/go.mod
index 1fd5989..86a2659 100644
--- a/go.mod
+++ b/go.mod
@@ -1,27 +1,23 @@
module github.com/variablenix/GoBot
-go 1.26.6
+go 1.26.9
require (
github.com/agnivade/levenshtein v1.2.1
- github.com/chromedp/chromedp v0.16.0
+ github.com/chromedp/chromedp v0.20.1
github.com/spf13/viper v1.21.0
go.etcd.io/bbolt v1.5.0
go.uber.org/zap v1.28.0
- golang.org/x/net v0.59.0
+ golang.org/x/net v0.60.0
golang.org/x/text v0.42.0
gopkg.in/irc.v3 v3.1.4
)
require (
- github.com/chromedp/cdproto v0.0.0-20260714215040-dc233986426f // indirect
- github.com/chromedp/sysutil v1.1.0 // indirect
+ github.com/chromedp/cdproto v0.157.8 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
- github.com/gobwas/httphead v0.1.0 // indirect
- github.com/gobwas/pool v0.2.1 // indirect
- github.com/gobwas/ws v1.4.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
diff --git a/go.sum b/go.sum
index 0abfc78..4d1ae13 100644
--- a/go.sum
+++ b/go.sum
@@ -2,12 +2,10 @@ github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KO
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
-github.com/chromedp/cdproto v0.0.0-20260714215040-dc233986426f h1:0Z1zcSLEmnj2c2CmJYBqewtS6pxhB39bNWUSEUAWjgk=
-github.com/chromedp/cdproto v0.0.0-20260714215040-dc233986426f/go.mod h1:RwFsSODCtFExll+GhHM6R92SARHR3Z3oipaxLHj46C0=
-github.com/chromedp/chromedp v0.16.0 h1:rOO4deOm4CbZgBCa8mD9g2rDyIoNs0BkgvNrlbp5ouk=
-github.com/chromedp/chromedp v0.16.0/go.mod h1:rbuGKFT1vMcFcFqKfPIO1GpX/N+2s8onm2qMxZLbU5U=
-github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
-github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
+github.com/chromedp/cdproto v0.157.8 h1:Tkq0AjsUlC3Z4sJfHBzV/uAAxnTF/AZyuSTjfV/1gvk=
+github.com/chromedp/cdproto v0.157.8/go.mod h1:h68JaIuFr9o3yiayxEYhBdvaxgYO3shDrPSYhNlyfu0=
+github.com/chromedp/chromedp v0.20.1 h1:983T9wmyM09JtROWlxhLQwljKOQovqt8CVE89W3U0NA=
+github.com/chromedp/chromedp v0.20.1/go.mod h1:6z1Y5FdKUxRzxx2qcC5h1xjaBJrSWrZp1mjgITfNZig=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -21,22 +19,12 @@ github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSy
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
-github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
-github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
-github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
-github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
-github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
-github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
-github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
-github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
-github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -71,11 +59,10 @@ go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
-golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
-golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
+golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
+golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
diff --git a/plugins/ask.go b/plugins/ask.go
index 098e105..90add59 100644
--- a/plugins/ask.go
+++ b/plugins/ask.go
@@ -1031,8 +1031,7 @@ func askDuckDuckGoRenderedSearchAssistOnce(ctx context.Context, question, fallba
browserCtx, browserCancel := chromedp.NewContext(allocatorCtx)
defer browserCancel()
- var result askRenderedSearchAssistData
- if err := chromedp.Run(browserCtx, chromedp.Navigate(duckDuckGoSearchAssistURL(question))); err != nil {
+ if err := chromedp.Do(browserCtx, chromedp.Navigate(duckDuckGoSearchAssistURL(question))); err != nil {
return askSource{}, false
}
// Navigation and browser startup have already consumed some of the budget.
@@ -1045,9 +1044,10 @@ func askDuckDuckGoRenderedSearchAssistOnce(ctx context.Context, question, fallba
if assistTimeout <= 0 {
return askSource{}, false
}
- if err := chromedp.Run(browserCtx, chromedp.PollFunction(askRenderedSearchAssistScript, &result,
+ result, err := chromedp.Run(browserCtx, chromedp.PollFunction[askRenderedSearchAssistData](askRenderedSearchAssistScript,
chromedp.WithPollingInterval(100*time.Millisecond),
- chromedp.WithPollingTimeout(assistTimeout))); err == nil {
+ chromedp.WithPollingTimeout(assistTimeout)))
+ if err == nil {
if source, ok := parseRenderedSearchAssist(result, fallbackURL); ok {
return source, true
}
@@ -1061,14 +1061,14 @@ func askDuckDuckGoRenderedSearchAssistOnce(ctx context.Context, question, fallba
if !fetchResults {
return askSource{}, false
}
- var results []askRenderedSearchResult
resultTimeout := time.Until(deadline)
if resultTimeout <= 0 {
return askSource{}, false
}
- if err := chromedp.Run(browserCtx, chromedp.PollFunction(askRenderedSearchResultsScript, &results,
+ results, err := chromedp.Run(browserCtx, chromedp.PollFunction[[]askRenderedSearchResult](askRenderedSearchResultsScript,
chromedp.WithPollingInterval(100*time.Millisecond),
- chromedp.WithPollingTimeout(resultTimeout))); err != nil {
+ chromedp.WithPollingTimeout(resultTimeout)))
+ if err != nil {
return askSource{}, false
}
selected, ok := selectAskSearchResult(question, results)
diff --git a/plugins/ask_resilience_test.go b/plugins/ask_resilience_test.go
index 7a601d4..c8e0032 100644
--- a/plugins/ask_resilience_test.go
+++ b/plugins/ask_resilience_test.go
@@ -2,6 +2,7 @@ package plugins
import (
"context"
+ "errors"
"net/http"
"net/url"
"os"
@@ -246,34 +247,35 @@ func TestAskBrowserBudgetLeavesFallbackTime(t *testing.T) {
// Run with GOBOT_TEST_BROWSER=/path/to/chromium. This fixture runs entirely
// offline and verifies the DOM code against a delayed, expanded answer card.
func TestAskRenderedCardWaitsForAnswer(t *testing.T) {
- executable := os.Getenv("GOBOT_TEST_BROWSER")
- if executable == "" {
- t.Skip("set GOBOT_TEST_BROWSER for offline browser fixture")
- }
- ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second)
- defer cancel()
- allocator, stop := chromedp.NewExecAllocator(ctx, append(chromedp.DefaultExecAllocatorOptions[:], chromedp.ExecPath(executable))...)
- defer stop()
- page, closePage := chromedp.NewContext(allocator)
- defer closePage()
- var before *askRenderedSearchAssistData
- var answer askRenderedSearchAssistData
+ page := askBrowserFixtureContext(t)
const fixture = `document.body.innerHTML='
2002
Source'`, nil), chromedp.Evaluate("("+askRenderedSearchAssistScript+")()", &answer)); err != nil { + if err := chromedp.Do(page, chromedp.Evaluate[chromedp.Void](`document.getElementById('card').innerHTML='2002
Source'`)); err != nil { + t.Fatal(err) + } + answer, err = chromedp.Run(page, chromedp.Evaluate[askRenderedSearchAssistData]("("+askRenderedSearchAssistScript+")()")) + if err != nil { t.Fatal(err) } if answer.Text != "2002" { @@ -281,6 +283,66 @@ func TestAskRenderedCardWaitsForAnswer(t *testing.T) { } } +func askBrowserFixtureContext(t *testing.T) context.Context { + t.Helper() + executable := os.Getenv("GOBOT_TEST_BROWSER") + if executable == "" { + t.Skip("set GOBOT_TEST_BROWSER for offline browser fixture") + } + ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second) + allocator, stop := chromedp.NewExecAllocator(ctx, append(chromedp.DefaultExecAllocatorOptions[:], chromedp.ExecPath(executable))...) + page, closePage := chromedp.NewContext(allocator) + t.Cleanup(func() { + closePage() + stop() + cancel() + }) + return page +} + +func TestAskRenderedResultsPollingPreservesFallbacks(t *testing.T) { + page := askBrowserFixtureContext(t) + if err := chromedp.Do(page, chromedp.Navigate("about:blank"), chromedp.Evaluate[chromedp.Void](`document.body.innerHTML=''`)); err != nil { + t.Fatal(err) + } + before, err := chromedp.Run(page, chromedp.Evaluate[[]askRenderedSearchResult]("("+askRenderedSearchResultsScript+")()")) + if err != nil { + t.Fatal(err) + } + if before != nil { + t.Fatalf("empty results accepted: %+v", before) + } + _, err = chromedp.Run(page, chromedp.PollFunction[[]askRenderedSearchResult](askRenderedSearchResultsScript, chromedp.WithPollingInterval(50*time.Millisecond), chromedp.WithPollingTimeout(150*time.Millisecond))) + if !errors.Is(err, chromedp.ErrPollingTimeout) || page.Err() != nil { + t.Fatalf("empty-result polling did not preserve the browser context: %v", err) + } + const delayed = `setTimeout(() => {document.getElementById('results').innerHTML='Compare equivalent models.
A public guide.