mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Add dictionary commands, antonym/etymology integrations, and audit fixes
New DictionaryPlugin with !antonym, !pronounce, !etymology, !difficulty, and !rhyme commands. Integrate antonyms into !define (concurrent 500ms fetch), difficulty tier into Hangman headers, and etymology into WOTD. Fix rhyme limit bug (request 50, display 7), variable shadowing, and add spec-required emoji prefixes to all dictionary command outputs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -215,6 +215,128 @@ func (c *Client) FrequencyBatch(words []string, lang string) (map[string]int, er
|
||||
return result.Frequencies, nil
|
||||
}
|
||||
|
||||
// Antonyms returns antonyms for a word in a language.
|
||||
func (c *Client) Antonyms(word, lang string) ([]string, error) {
|
||||
u := c.baseURL + "/antonyms?" + url.Values{"word": {word}, "lang": {lang}}.Encode()
|
||||
resp, err := c.httpClient.Get(u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dreamdict: antonyms: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("dreamdict: antonyms: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Antonyms []string `json:"antonyms"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("dreamdict: antonyms: decode: %w", err)
|
||||
}
|
||||
return result.Antonyms, nil
|
||||
}
|
||||
|
||||
// Pronunciation holds pronunciation data for a word.
|
||||
type Pronunciation struct {
|
||||
Format string `json:"format"`
|
||||
Value string `json:"value"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
// Pronunciations returns pronunciation data for a word in a language.
|
||||
func (c *Client) Pronunciations(word, lang string) ([]Pronunciation, error) {
|
||||
u := c.baseURL + "/pronunciation?" + url.Values{"word": {word}, "lang": {lang}}.Encode()
|
||||
resp, err := c.httpClient.Get(u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dreamdict: pronunciation: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("dreamdict: pronunciation: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Pronunciations []Pronunciation `json:"pronunciations"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("dreamdict: pronunciation: decode: %w", err)
|
||||
}
|
||||
return result.Pronunciations, nil
|
||||
}
|
||||
|
||||
// Etymology returns the etymology text for a word in a language.
|
||||
func (c *Client) Etymology(word, lang string) (string, error) {
|
||||
u := c.baseURL + "/etymology?" + url.Values{"word": {word}, "lang": {lang}}.Encode()
|
||||
resp, err := c.httpClient.Get(u)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("dreamdict: etymology: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("dreamdict: etymology: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Etymology string `json:"etymology"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", fmt.Errorf("dreamdict: etymology: decode: %w", err)
|
||||
}
|
||||
return result.Etymology, nil
|
||||
}
|
||||
|
||||
// Difficulty returns the difficulty score (0.0-1.0) for a word in a language.
|
||||
// Returns -1 if the word has no difficulty score.
|
||||
func (c *Client) Difficulty(word, lang string) (float64, error) {
|
||||
u := c.baseURL + "/difficulty?" + url.Values{"word": {word}, "lang": {lang}}.Encode()
|
||||
resp, err := c.httpClient.Get(u)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("dreamdict: difficulty: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return 0, fmt.Errorf("dreamdict: difficulty: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Difficulty float64 `json:"difficulty"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return 0, fmt.Errorf("dreamdict: difficulty: decode: %w", err)
|
||||
}
|
||||
return result.Difficulty, nil
|
||||
}
|
||||
|
||||
// Rhymes returns words that rhyme with the given word (English only).
|
||||
func (c *Client) Rhymes(word string, limit int) ([]string, error) {
|
||||
params := url.Values{"word": {word}}
|
||||
if limit > 0 {
|
||||
params.Set("limit", fmt.Sprintf("%d", limit))
|
||||
}
|
||||
u := c.baseURL + "/rhyme?" + params.Encode()
|
||||
resp, err := c.httpClient.Get(u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dreamdict: rhyme: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("dreamdict: rhyme: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Rhymes []string `json:"rhymes"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("dreamdict: rhyme: decode: %w", err)
|
||||
}
|
||||
return result.Rhymes, 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")
|
||||
|
||||
367
internal/plugin/dictionary.go
Normal file
367
internal/plugin/dictionary.go
Normal file
@@ -0,0 +1,367 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"gogobee/internal/dreamclient"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
)
|
||||
|
||||
// DictionaryPlugin provides extended dictionary commands using DreamDict.
|
||||
type DictionaryPlugin struct {
|
||||
Base
|
||||
dict *dreamclient.Client
|
||||
}
|
||||
|
||||
func NewDictionaryPlugin(client *mautrix.Client, dict *dreamclient.Client) *DictionaryPlugin {
|
||||
return &DictionaryPlugin{
|
||||
Base: NewBase(client),
|
||||
dict: dict,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DictionaryPlugin) Name() string { return "dictionary" }
|
||||
|
||||
func (p *DictionaryPlugin) Commands() []CommandDef {
|
||||
return []CommandDef{
|
||||
{Name: "antonym", Description: "Look up antonyms for a word", Usage: "!antonym <word> [lang]", Category: "Lookup & Reference"},
|
||||
{Name: "pronounce", Description: "Look up pronunciation of a word", Usage: "!pronounce <word> [lang]", Category: "Lookup & Reference"},
|
||||
{Name: "etymology", Description: "Look up the etymology of a word", Usage: "!etymology <word> [lang]", Category: "Lookup & Reference"},
|
||||
{Name: "difficulty", Description: "Show difficulty score for a word", Usage: "!difficulty <word> [lang]", Category: "Lookup & Reference"},
|
||||
{Name: "rhyme", Description: "Find rhyming words (English)", Usage: "!rhyme <word>", Category: "Lookup & Reference"},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DictionaryPlugin) Init() error { return nil }
|
||||
|
||||
func (p *DictionaryPlugin) OnReaction(_ ReactionContext) error { return nil }
|
||||
|
||||
func (p *DictionaryPlugin) OnMessage(ctx MessageContext) error {
|
||||
var handler func(MessageContext) error
|
||||
switch {
|
||||
case p.IsCommand(ctx.Body, "antonym"):
|
||||
handler = p.handleAntonym
|
||||
case p.IsCommand(ctx.Body, "pronounce"):
|
||||
handler = p.handlePronounce
|
||||
case p.IsCommand(ctx.Body, "etymology"):
|
||||
handler = p.handleEtymology
|
||||
case p.IsCommand(ctx.Body, "difficulty"):
|
||||
handler = p.handleDifficulty
|
||||
case p.IsCommand(ctx.Body, "rhyme"):
|
||||
handler = p.handleRhyme
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
go func() {
|
||||
if err := handler(ctx); err != nil {
|
||||
slog.Error("dictionary: handler error", "err", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
const dictUnavailable = "Dictionary service unavailable. Try again shortly."
|
||||
const dictSupportedLangs = "en, fr, pt-PT, zh"
|
||||
|
||||
// parseWordLang extracts word and language from command args. Returns empty word if missing.
|
||||
func parseWordLang(args string) (word, lang string) {
|
||||
parts := strings.Fields(args)
|
||||
if len(parts) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
word = strings.ToLower(parts[0])
|
||||
lang = "en"
|
||||
if len(parts) >= 2 {
|
||||
if l := normaliseLangExt(parts[len(parts)-1]); l != "" {
|
||||
lang = l
|
||||
if len(parts) > 2 {
|
||||
word = strings.ToLower(strings.Join(parts[:len(parts)-1], " "))
|
||||
}
|
||||
}
|
||||
}
|
||||
return word, lang
|
||||
}
|
||||
|
||||
// normaliseLangExt normalises a language tag, including zh.
|
||||
func normaliseLangExt(s string) string {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
switch s {
|
||||
case "en":
|
||||
return "en"
|
||||
case "fr":
|
||||
return "fr"
|
||||
case "pt-pt", "pt":
|
||||
return "pt-PT"
|
||||
case "zh":
|
||||
return "zh"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DictionaryPlugin) handleAntonym(ctx MessageContext) error {
|
||||
args := strings.TrimSpace(p.GetArgs(ctx.Body, "antonym"))
|
||||
if args == "" {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
"Usage: `!antonym <word> [lang]`\nSupported languages: "+dictSupportedLangs)
|
||||
}
|
||||
|
||||
word, lang := parseWordLang(args)
|
||||
if p.dict == nil {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, dictUnavailable)
|
||||
}
|
||||
|
||||
// Check word exists.
|
||||
valid, err := p.dict.IsValidWord(word, lang)
|
||||
if err != nil {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, dictUnavailable)
|
||||
}
|
||||
if !valid {
|
||||
langName := langDisplayName(lang)
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
fmt.Sprintf("\"**%s**\" not found in %s.", word, langName))
|
||||
}
|
||||
|
||||
ants, err := p.dict.Antonyms(word, lang)
|
||||
if err != nil {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, dictUnavailable)
|
||||
}
|
||||
|
||||
if len(ants) == 0 {
|
||||
langName := langDisplayName(lang)
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
fmt.Sprintf("🔄 No antonyms found for \"**%s**\" in %s.", word, langName))
|
||||
}
|
||||
|
||||
display := ants
|
||||
more := ""
|
||||
if len(display) > 6 {
|
||||
more = fmt.Sprintf(" (+%d more)", len(display)-6)
|
||||
display = display[:6]
|
||||
}
|
||||
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
fmt.Sprintf("🔄 Antonyms of **%s** (%s): %s%s", word, lang, strings.Join(display, ", "), more))
|
||||
}
|
||||
|
||||
func (p *DictionaryPlugin) handlePronounce(ctx MessageContext) error {
|
||||
args := strings.TrimSpace(p.GetArgs(ctx.Body, "pronounce"))
|
||||
if args == "" {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
"Usage: `!pronounce <word> [lang]`\nSupported languages: "+dictSupportedLangs)
|
||||
}
|
||||
|
||||
word, lang := parseWordLang(args)
|
||||
if p.dict == nil {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, dictUnavailable)
|
||||
}
|
||||
|
||||
valid, err := p.dict.IsValidWord(word, lang)
|
||||
if err != nil {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, dictUnavailable)
|
||||
}
|
||||
if !valid {
|
||||
langName := langDisplayName(lang)
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
fmt.Sprintf("\"**%s**\" not found in %s.", word, langName))
|
||||
}
|
||||
|
||||
prons, err := p.dict.Pronunciations(word, lang)
|
||||
if err != nil {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, dictUnavailable)
|
||||
}
|
||||
|
||||
if len(prons) == 0 {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
fmt.Sprintf("🔊 No pronunciation data found for \"**%s**\" in %s.", word, langDisplayName(lang)))
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("🔊 **%s** (%s)\n", word, lang))
|
||||
|
||||
for _, pr := range prons {
|
||||
label := strings.ToUpper(pr.Format)
|
||||
if lang == "zh" && pr.Format == "ipa" {
|
||||
label = "Pinyin"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" %s: %s\n", label, pr.Value))
|
||||
}
|
||||
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
|
||||
}
|
||||
|
||||
func (p *DictionaryPlugin) handleEtymology(ctx MessageContext) error {
|
||||
args := strings.TrimSpace(p.GetArgs(ctx.Body, "etymology"))
|
||||
if args == "" {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
"Usage: `!etymology <word> [lang]`\nSupported languages: "+dictSupportedLangs)
|
||||
}
|
||||
|
||||
word, lang := parseWordLang(args)
|
||||
if p.dict == nil {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, dictUnavailable)
|
||||
}
|
||||
|
||||
valid, err := p.dict.IsValidWord(word, lang)
|
||||
if err != nil {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, dictUnavailable)
|
||||
}
|
||||
if !valid {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
fmt.Sprintf("\"**%s**\" not found in %s.", word, langDisplayName(lang)))
|
||||
}
|
||||
|
||||
etym, err := p.dict.Etymology(word, lang)
|
||||
if err != nil {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, dictUnavailable)
|
||||
}
|
||||
|
||||
if etym == "" {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
fmt.Sprintf("📜 No etymology data found for \"**%s**\" in %s.", word, langDisplayName(lang)))
|
||||
}
|
||||
|
||||
// Truncate at 400 chars for standalone command.
|
||||
if len(etym) > 400 {
|
||||
// Try to truncate at sentence boundary.
|
||||
if idx := strings.LastIndex(etym[:400], "."); idx > 200 {
|
||||
etym = etym[:idx+1] + "..."
|
||||
} else {
|
||||
etym = etym[:400] + "..."
|
||||
}
|
||||
}
|
||||
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
fmt.Sprintf("📜 Etymology of **%s** (%s)\n %s", word, lang, etym))
|
||||
}
|
||||
|
||||
func (p *DictionaryPlugin) handleDifficulty(ctx MessageContext) error {
|
||||
args := strings.TrimSpace(p.GetArgs(ctx.Body, "difficulty"))
|
||||
if args == "" {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
"Usage: `!difficulty <word> [lang]`\nSupported languages: "+dictSupportedLangs)
|
||||
}
|
||||
|
||||
word, lang := parseWordLang(args)
|
||||
if p.dict == nil {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, dictUnavailable)
|
||||
}
|
||||
|
||||
valid, err := p.dict.IsValidWord(word, lang)
|
||||
if err != nil {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, dictUnavailable)
|
||||
}
|
||||
if !valid {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
fmt.Sprintf("\"**%s**\" not found in %s.", word, langDisplayName(lang)))
|
||||
}
|
||||
|
||||
diff, err := p.dict.Difficulty(word, lang)
|
||||
if err != nil {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, dictUnavailable)
|
||||
}
|
||||
if diff < 0 {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
fmt.Sprintf("📊 No difficulty score available for \"**%s**\" in %s.", word, langDisplayName(lang)))
|
||||
}
|
||||
|
||||
tier := difficultyTier(diff)
|
||||
|
||||
// Get frequency for display.
|
||||
freq, _ := p.dict.Frequency(word, lang)
|
||||
freqLabel := frequencyLabel(freq)
|
||||
|
||||
wordLen := len([]rune(word))
|
||||
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
fmt.Sprintf("📊 **%s** (%s) -- %s (%.2f)\n Frequency: %s | Length: %d",
|
||||
word, lang, tier, diff, freqLabel, wordLen))
|
||||
}
|
||||
|
||||
func (p *DictionaryPlugin) handleRhyme(ctx MessageContext) error {
|
||||
args := strings.TrimSpace(p.GetArgs(ctx.Body, "rhyme"))
|
||||
if args == "" {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
"Usage: `!rhyme <word>`\nNote: rhyme matching is English only.")
|
||||
}
|
||||
|
||||
parts := strings.Fields(args)
|
||||
word := strings.ToLower(parts[0])
|
||||
showAll := false
|
||||
for _, arg := range parts[1:] {
|
||||
if arg == "--all" {
|
||||
showAll = true
|
||||
}
|
||||
}
|
||||
|
||||
if p.dict == nil {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, dictUnavailable)
|
||||
}
|
||||
|
||||
rhymes, err := p.dict.Rhymes(word, 50)
|
||||
if err != nil {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, dictUnavailable)
|
||||
}
|
||||
|
||||
if len(rhymes) == 0 {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
fmt.Sprintf("🎵 No rhymes found for \"**%s**\". (This is not surprising.)", word))
|
||||
}
|
||||
|
||||
display := rhymes
|
||||
more := ""
|
||||
if !showAll && len(display) > 7 {
|
||||
more = fmt.Sprintf(" (+%d more)\n Use `!rhyme %s --all` to see all results", len(display)-7, word)
|
||||
display = display[:7]
|
||||
}
|
||||
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||
fmt.Sprintf("🎵 Rhymes with **%s**: %s%s", word, strings.Join(display, ", "), more))
|
||||
}
|
||||
|
||||
// difficultyTier maps a 0.0-1.0 score to a human-readable label.
|
||||
func difficultyTier(score float64) string {
|
||||
switch {
|
||||
case score <= 0.25:
|
||||
return "Easy"
|
||||
case score <= 0.50:
|
||||
return "Medium"
|
||||
case score <= 0.75:
|
||||
return "Hard"
|
||||
default:
|
||||
return "Brutal"
|
||||
}
|
||||
}
|
||||
|
||||
// frequencyLabel maps a frequency score to a display label.
|
||||
func frequencyLabel(freq int) string {
|
||||
switch {
|
||||
case freq > 500:
|
||||
return "high"
|
||||
case freq >= 100:
|
||||
return "medium"
|
||||
case freq > 0:
|
||||
return "low"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// langDisplayName returns a human-readable name for a language code.
|
||||
func langDisplayName(lang string) string {
|
||||
switch lang {
|
||||
case "en":
|
||||
return "English"
|
||||
case "fr":
|
||||
return "French"
|
||||
case "pt-PT":
|
||||
return "Portuguese"
|
||||
case "zh":
|
||||
return "Chinese"
|
||||
default:
|
||||
return lang
|
||||
}
|
||||
}
|
||||
@@ -615,6 +615,14 @@ func (p *HangmanPlugin) startMultilingualGame(ctx MessageContext, lang, clueLang
|
||||
p.games[ctx.RoomID] = game
|
||||
p.mu.Unlock()
|
||||
|
||||
// Fetch difficulty tier from DreamDict.
|
||||
diffLabel := ""
|
||||
if p.dict != nil {
|
||||
if diff, err := p.dict.Difficulty(strings.ToLower(word), lang); err == nil && diff >= 0 {
|
||||
diffLabel = " (" + difficultyTier(diff) + ")"
|
||||
}
|
||||
}
|
||||
|
||||
// Build thread root
|
||||
var sb strings.Builder
|
||||
if clueLang != "" {
|
||||
@@ -622,9 +630,9 @@ func (p *HangmanPlugin) startMultilingualGame(ctx MessageContext, lang, clueLang
|
||||
if clueLangName == "" {
|
||||
clueLangName = clueLang
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("🐝 **Hangman — %s** (clue: %s)\n", langName, clueLangName))
|
||||
sb.WriteString(fmt.Sprintf("🐝 **Hangman — %s%s** (clue: %s)\n", langName, diffLabel, clueLangName))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("🐝 **Hangman — %s**\n", langName))
|
||||
sb.WriteString(fmt.Sprintf("🐝 **Hangman — %s%s**\n", langName, diffLabel))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("Word: %s (%d letters)\n", game.displayPhrase(), len([]rune(word))))
|
||||
|
||||
|
||||
@@ -193,6 +193,37 @@ func (p *LookupPlugin) handleDefine(ctx MessageContext) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Concurrently fetch antonyms from DreamDict (500ms timeout).
|
||||
if p.dict != nil {
|
||||
type antResult struct {
|
||||
ants []string
|
||||
}
|
||||
ch := make(chan antResult, 1)
|
||||
go func() {
|
||||
ants, err := p.dict.Antonyms(strings.ToLower(word), "en")
|
||||
if err != nil {
|
||||
ch <- antResult{}
|
||||
return
|
||||
}
|
||||
ch <- antResult{ants: ants}
|
||||
}()
|
||||
|
||||
timer := time.NewTimer(500 * time.Millisecond)
|
||||
select {
|
||||
case r := <-ch:
|
||||
timer.Stop()
|
||||
if len(r.ants) > 0 {
|
||||
display := r.ants
|
||||
if len(display) > 3 {
|
||||
display = display[:3]
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("\nAntonyms: %s\n", strings.Join(display, ", ")))
|
||||
}
|
||||
case <-timer.C:
|
||||
// Timeout — omit antonyms silently.
|
||||
}
|
||||
}
|
||||
|
||||
msg := sb.String()
|
||||
db.CacheSet(cacheKey, msg)
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
||||
|
||||
@@ -160,6 +160,19 @@ func (p *WOTDPlugin) pickWord(lang string) (string, string, string, string) {
|
||||
transMap["syn"] = synonyms
|
||||
}
|
||||
|
||||
// Fetch etymology
|
||||
if etym, err := p.dict.Etymology(candidate, lang); err == nil && etym != "" {
|
||||
// Truncate for WOTD display (max 200 chars).
|
||||
if len(etym) > 200 {
|
||||
if idx := strings.LastIndex(etym[:200], "."); idx > 100 {
|
||||
etym = etym[:idx+1] + "..."
|
||||
} else {
|
||||
etym = etym[:200] + "..."
|
||||
}
|
||||
}
|
||||
transMap["_etym"] = []string{etym}
|
||||
}
|
||||
|
||||
var translationsJSON string
|
||||
if data, err := json.Marshal(transMap); err == nil {
|
||||
translationsJSON = string(data)
|
||||
@@ -414,6 +427,13 @@ func (p *WOTDPlugin) formatWOTD(word, definition, partOfSpeech, translationsJSON
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// Etymology section (if available).
|
||||
if transMap != nil {
|
||||
if etym, ok := transMap["_etym"]; ok && len(etym) > 0 && etym[0] != "" {
|
||||
sb.WriteString(fmt.Sprintf("Etymology\n %s\n\n", etym[0]))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("━━━━━━━━━━━━━━━━━━━━\n")
|
||||
sb.WriteString(fmt.Sprintf("Learn more: `!define %s %s`\n", word, lang))
|
||||
sb.WriteString("Use this word in a message today to earn 25 XP!")
|
||||
|
||||
1
main.go
1
main.go
@@ -112,6 +112,7 @@ func main() {
|
||||
// Entertainment / Lookup
|
||||
registry.Register(plugin.NewRetroPlugin(client))
|
||||
registry.Register(plugin.NewLookupPlugin(client, ratePlugin, dictClient))
|
||||
registry.Register(plugin.NewDictionaryPlugin(client, dictClient))
|
||||
registry.Register(plugin.NewCountdownPlugin(client))
|
||||
registry.Register(plugin.NewStocksPlugin(client))
|
||||
forexPlugin := plugin.NewForexPlugin(client)
|
||||
|
||||
Reference in New Issue
Block a user