From 31a1d00236e349e0a1ca0c237e1cf1c4245f061c Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:24:24 -0700 Subject: [PATCH] Add DreamDict API client for dictionary and translation lookups Standalone HTTP client for the DreamDict service supporting word lookup, random word generation, and cross-language translation (en/fr/pt-PT). Used by hangman, lookup, and WOTD plugins. Co-Authored-By: Claude Opus 4.6 --- internal/dreamclient/client.go | 186 +++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 internal/dreamclient/client.go diff --git a/internal/dreamclient/client.go b/internal/dreamclient/client.go new file mode 100644 index 0000000..46b26ff --- /dev/null +++ b/internal/dreamclient/client.go @@ -0,0 +1,186 @@ +package dreamclient + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + "time" +) + +// Client is an HTTP client for the DreamDict dictionary service. +type Client struct { + baseURL string + httpClient *http.Client +} + +// New creates a new DreamDict client. +func New(baseURL string) *Client { + return &Client{ + baseURL: baseURL, + httpClient: &http.Client{Timeout: 3 * time.Second}, + } +} + +// Definition is a single definition entry from DreamDict. +type Definition struct { + POS string `json:"pos"` + Gloss string `json:"gloss"` + Source string `json:"source"` + Priority int `json:"priority"` +} + +// HealthResponse is the response from /health. +type HealthResponse struct { + Status string `json:"status"` + DBPath string `json:"db_path"` + WordCounts map[string]int `json:"word_counts"` + DefCounts map[string]int `json:"def_counts"` + ImportedAt string `json:"imported_at"` + SchemaVersion string `json:"schema_version"` +} + +// IsValidWord checks if a word exists in a language's word list. +func (c *Client) IsValidWord(word, lang string) (bool, error) { + u := c.baseURL + "/valid?" + url.Values{"word": {word}, "lang": {lang}}.Encode() + resp, err := c.httpClient.Get(u) + if err != nil { + return false, fmt.Errorf("dreamdict: valid: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return false, fmt.Errorf("dreamdict: valid: status %d", resp.StatusCode) + } + + var result struct { + Valid bool `json:"valid"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return false, fmt.Errorf("dreamdict: valid: decode: %w", err) + } + return result.Valid, nil +} + +// RandomWord returns a random word for a language with optional filters. +// pos can be empty. min/max of 0 means no filter. +func (c *Client) RandomWord(lang, pos string, min, max int) (string, error) { + params := url.Values{"lang": {lang}} + if pos != "" { + params.Set("pos", pos) + } + if min > 0 { + params.Set("min", strconv.Itoa(min)) + } + if max > 0 { + params.Set("max", strconv.Itoa(max)) + } + + u := c.baseURL + "/random?" + params.Encode() + resp, err := c.httpClient.Get(u) + if err != nil { + return "", fmt.Errorf("dreamdict: random: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return "", fmt.Errorf("dreamdict: random: no match") + } + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("dreamdict: random: status %d", resp.StatusCode) + } + + var result struct { + Word string `json:"word"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "", fmt.Errorf("dreamdict: random: decode: %w", err) + } + return result.Word, nil +} + +// Define returns all definitions for a word in a language. +func (c *Client) Define(word, lang string) ([]Definition, error) { + u := c.baseURL + "/define?" + url.Values{"word": {word}, "lang": {lang}}.Encode() + resp, err := c.httpClient.Get(u) + if err != nil { + return nil, fmt.Errorf("dreamdict: define: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("dreamdict: define: status %d", resp.StatusCode) + } + + var result struct { + Definitions []Definition `json:"definitions"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("dreamdict: define: decode: %w", err) + } + return result.Definitions, nil +} + +// Synonyms returns synonyms for a word in a language. +func (c *Client) Synonyms(word, lang string) ([]string, error) { + u := c.baseURL + "/synonyms?" + url.Values{"word": {word}, "lang": {lang}}.Encode() + resp, err := c.httpClient.Get(u) + if err != nil { + return nil, fmt.Errorf("dreamdict: synonyms: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("dreamdict: synonyms: status %d", resp.StatusCode) + } + + var result struct { + Synonyms []string `json:"synonyms"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("dreamdict: synonyms: decode: %w", err) + } + return result.Synonyms, nil +} + +// Translate returns translations of a word from one language to another. +func (c *Client) Translate(word, from, to string) ([]string, error) { + u := c.baseURL + "/translate?" + url.Values{"word": {word}, "from": {from}, "to": {to}}.Encode() + resp, err := c.httpClient.Get(u) + if err != nil { + return nil, fmt.Errorf("dreamdict: translate: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("dreamdict: translate: status %d", resp.StatusCode) + } + + var result struct { + Translations []string `json:"translations"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("dreamdict: translate: decode: %w", err) + } + return result.Translations, nil +} + +// Health checks if the DreamDict service is reachable and returns stats. +func (c *Client) Health() (*HealthResponse, error) { + resp, err := c.httpClient.Get(c.baseURL + "/health") + if err != nil { + return nil, fmt.Errorf("dreamdict: health: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("dreamdict: health: status %d", resp.StatusCode) + } + + var result HealthResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("dreamdict: health: decode: %w", err) + } + return &result, nil +}