feat: add Stack Overflow search engine

Uses the Stack Exchange API v3 (/search/advanced) to find questions
sorted by relevance. No API key required (300 req/day); optionally
configure via STACKOVERFLOW_KEY env var or [engines.stackoverflow].

Results include score, answer count, view count, and tags in the
snippet. Assigned to the 'it' category, triggered by the IT category
tab or explicit engine selection.

6 tests covering parsing, edge cases, and helpers.
This commit is contained in:
Franz Kafka 2026-03-22 22:29:34 +00:00
parent e96040ef35
commit df67492602
5 changed files with 440 additions and 7 deletions

View file

@ -50,10 +50,15 @@ type UpstreamConfig struct {
}
type EnginesConfig struct {
LocalPorted []string `toml:"local_ported"`
Brave BraveConfig `toml:"brave"`
Qwant QwantConfig `toml:"qwant"`
YouTube YouTubeConfig `toml:"youtube"`
LocalPorted []string `toml:"local_ported"`
Brave BraveConfig `toml:"brave"`
Qwant QwantConfig `toml:"qwant"`
YouTube YouTubeConfig `toml:"youtube"`
StackOverflow *StackOverflowConfig `toml:"stackoverflow"`
}
type StackOverflowConfig struct {
APIKey string `toml:"api_key"`
}
// CacheConfig holds Valkey/Redis cache settings.
@ -205,6 +210,12 @@ func applyEnvOverrides(cfg *Config) {
if v := os.Getenv("YOUTUBE_API_KEY"); v != "" {
cfg.Engines.YouTube.APIKey = v
}
if v := os.Getenv("STACKOVERFLOW_KEY"); v != "" {
if cfg.Engines.StackOverflow == nil {
cfg.Engines.StackOverflow = &StackOverflowConfig{}
}
cfg.Engines.StackOverflow.APIKey = v
}
if v := os.Getenv("VALKEY_ADDRESS"); v != "" {
cfg.Cache.Address = v
}