mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
Add DreamDict integrations: translate, hangman clues, and Portuguese WOTD
- Replace LibreTranslate-based !translate with DreamDict cross-language lookup (en/fr/pt-PT) with auto-detect and 5-per-lang cap - Add multilingual hangman mode (!hangman <lang> [--clue <lang>]) using DreamDict RandomWord/Translate with retry logic and race-safe placeholder - Rewrite WOTD to pick pt-PT words with Portuguese definitions and en/fr translations, add !wotd force for moderators - Move DreamDict client init earlier in main.go for broader availability - Wire dictClient into LookupPlugin, HangmanPlugin, and WOTDPlugin Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
|||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"gogobee/internal/db"
|
"gogobee/internal/db"
|
||||||
|
"gogobee/internal/dreamclient"
|
||||||
|
|
||||||
"maunium.net/go/mautrix"
|
"maunium.net/go/mautrix"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
@@ -121,6 +122,9 @@ type hangmanGame struct {
|
|||||||
solvedBy id.UserID
|
solvedBy id.UserID
|
||||||
earlySolve bool
|
earlySolve bool
|
||||||
threadID id.EventID // thread root event for this game
|
threadID id.EventID // thread root event for this game
|
||||||
|
lang string // puzzle word language (e.g. "pt-PT")
|
||||||
|
clueLang string // clue language (e.g. "en")
|
||||||
|
clueWord string // translated clue word
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHangmanGame(phrase string, maxWrong int) *hangmanGame {
|
func newHangmanGame(phrase string, maxWrong int) *hangmanGame {
|
||||||
@@ -272,6 +276,7 @@ func (g *hangmanGame) wrongGuessStr() string {
|
|||||||
type HangmanPlugin struct {
|
type HangmanPlugin struct {
|
||||||
Base
|
Base
|
||||||
euro *EuroPlugin
|
euro *EuroPlugin
|
||||||
|
dict *dreamclient.Client
|
||||||
phrases []string
|
phrases []string
|
||||||
maxWrong int
|
maxWrong int
|
||||||
bonusMul float64
|
bonusMul float64
|
||||||
@@ -280,10 +285,11 @@ type HangmanPlugin struct {
|
|||||||
games map[id.RoomID]*hangmanGame
|
games map[id.RoomID]*hangmanGame
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHangmanPlugin(client *mautrix.Client, euro *EuroPlugin) *HangmanPlugin {
|
func NewHangmanPlugin(client *mautrix.Client, euro *EuroPlugin, dict *dreamclient.Client) *HangmanPlugin {
|
||||||
return &HangmanPlugin{
|
return &HangmanPlugin{
|
||||||
Base: NewBase(client),
|
Base: NewBase(client),
|
||||||
euro: euro,
|
euro: euro,
|
||||||
|
dict: dict,
|
||||||
maxWrong: envInt("HANGMAN_MAX_WRONG_GUESSES", 6),
|
maxWrong: envInt("HANGMAN_MAX_WRONG_GUESSES", 6),
|
||||||
bonusMul: envFloat("HANGMAN_SOLUTION_BONUS_MULTIPLIER", 2),
|
bonusMul: envFloat("HANGMAN_SOLUTION_BONUS_MULTIPLIER", 2),
|
||||||
games: make(map[id.RoomID]*hangmanGame),
|
games: make(map[id.RoomID]*hangmanGame),
|
||||||
@@ -294,7 +300,7 @@ func (p *HangmanPlugin) Name() string { return "hangman" }
|
|||||||
|
|
||||||
func (p *HangmanPlugin) Commands() []CommandDef {
|
func (p *HangmanPlugin) Commands() []CommandDef {
|
||||||
return []CommandDef{
|
return []CommandDef{
|
||||||
{Name: "hangman", Description: "Collaborative Hangman game", Usage: "!hangman start [easy|medium|hard|extreme] | !hangman [letter/phrase] | !hangman submit [phrase]", Category: "Games"},
|
{Name: "hangman", Description: "Collaborative Hangman game", Usage: "!hangman start [easy|medium|hard|extreme] | !hangman [lang] [--clue <lang>] | !hangman submit [phrase]", Category: "Games"},
|
||||||
{Name: "hangboard", Description: "Hangman leaderboard", Usage: "!hangboard", Category: "Games"},
|
{Name: "hangboard", Description: "Hangman leaderboard", Usage: "!hangboard", Category: "Games"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -347,18 +353,49 @@ func (p *HangmanPlugin) OnMessage(ctx MessageContext) error {
|
|||||||
lower := strings.ToLower(args)
|
lower := strings.ToLower(args)
|
||||||
switch {
|
switch {
|
||||||
case args == "" || lower == "start":
|
case args == "" || lower == "start":
|
||||||
return p.handleStart(ctx, "")
|
return p.handleStart(ctx, "", "", "")
|
||||||
case strings.HasPrefix(lower, "start "):
|
case strings.HasPrefix(lower, "start "):
|
||||||
return p.handleStart(ctx, strings.TrimSpace(args[6:]))
|
return p.handleStart(ctx, strings.TrimSpace(args[6:]), "", "")
|
||||||
case strings.HasPrefix(lower, "submit "):
|
case strings.HasPrefix(lower, "submit "):
|
||||||
return p.handleSubmit(ctx, strings.TrimSpace(args[7:]))
|
return p.handleSubmit(ctx, strings.TrimSpace(args[7:]))
|
||||||
case lower == "skip":
|
case lower == "skip":
|
||||||
return p.handleSkip(ctx)
|
return p.handleSkip(ctx)
|
||||||
default:
|
default:
|
||||||
|
// Check if this is a multilingual start: !hangman <lang> [--clue <lang>]
|
||||||
|
if lang, clueLang, ok := p.parseMultilingualArgs(args); ok {
|
||||||
|
return p.handleStart(ctx, "", lang, clueLang)
|
||||||
|
}
|
||||||
return p.handleGuess(ctx, args)
|
return p.handleGuess(ctx, args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseMultilingualArgs parses "pt-PT --clue en" or "fr --clue en" style args.
|
||||||
|
// Returns (lang, clueLang, true) if the first token is a valid language tag.
|
||||||
|
func (p *HangmanPlugin) parseMultilingualArgs(args string) (string, string, bool) {
|
||||||
|
parts := strings.Fields(args)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
lang := normaliseLang(parts[0])
|
||||||
|
if lang == "" {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
clueLang := ""
|
||||||
|
for i := 1; i < len(parts); i++ {
|
||||||
|
if strings.ToLower(parts[i]) == "--clue" && i+1 < len(parts) {
|
||||||
|
clueLang = normaliseLang(parts[i+1])
|
||||||
|
if clueLang == "" {
|
||||||
|
return lang, "", false // invalid clue lang, will be handled in handleStart
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lang, clueLang, true
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Phrase management
|
// Phrase management
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -415,7 +452,7 @@ func (p *HangmanPlugin) addPhrase(phrase string) error {
|
|||||||
// Game commands
|
// Game commands
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func (p *HangmanPlugin) handleStart(ctx MessageContext, difficulty string) error {
|
func (p *HangmanPlugin) handleStart(ctx MessageContext, difficulty, lang, clueLang string) error {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
|
|
||||||
if _, active := p.games[ctx.RoomID]; active {
|
if _, active := p.games[ctx.RoomID]; active {
|
||||||
@@ -423,6 +460,31 @@ func (p *HangmanPlugin) handleStart(ctx MessageContext, difficulty string) error
|
|||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "A Hangman game is already in progress!")
|
return p.SendReply(ctx.RoomID, ctx.EventID, "A Hangman game is already in progress!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multilingual mode: use DreamDict for word selection
|
||||||
|
if lang != "" {
|
||||||
|
// Validate before reserving the slot
|
||||||
|
if p.dict == nil {
|
||||||
|
p.mu.Unlock()
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, "❌ Dictionary service unavailable. Try again shortly.")
|
||||||
|
}
|
||||||
|
if clueLang != "" && clueLang == lang {
|
||||||
|
p.mu.Unlock()
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, "❌ Clue language must differ from the puzzle language.")
|
||||||
|
}
|
||||||
|
if clueLang != "" {
|
||||||
|
if normaliseLang(clueLang) == "" {
|
||||||
|
p.mu.Unlock()
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||||
|
fmt.Sprintf("❌ Unknown clue language \"%s\". Supported: en, fr, pt-PT", clueLang))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reserve the slot so no concurrent start can race us during network I/O
|
||||||
|
p.games[ctx.RoomID] = &hangmanGame{} // placeholder
|
||||||
|
p.mu.Unlock()
|
||||||
|
return p.startMultilingualGame(ctx, lang, clueLang)
|
||||||
|
}
|
||||||
|
|
||||||
if len(p.phrases) == 0 {
|
if len(p.phrases) == 0 {
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "No phrases loaded. Ask an admin to set up HANGMAN_PHRASE_FILE.")
|
return p.SendReply(ctx.RoomID, ctx.EventID, "No phrases loaded. Ask an admin to set up HANGMAN_PHRASE_FILE.")
|
||||||
@@ -493,6 +555,107 @@ func (p *HangmanPlugin) handleStart(ctx MessageContext, difficulty string) error
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// startMultilingualGame picks a DreamDict word and starts a multilingual hangman game.
|
||||||
|
// Caller must have already reserved p.games[ctx.RoomID] with a placeholder.
|
||||||
|
func (p *HangmanPlugin) startMultilingualGame(ctx MessageContext, lang, clueLang string) error {
|
||||||
|
rollback := func() {
|
||||||
|
p.mu.Lock()
|
||||||
|
if g := p.games[ctx.RoomID]; g != nil && g.phrase == "" {
|
||||||
|
delete(p.games, ctx.RoomID)
|
||||||
|
}
|
||||||
|
p.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
langName := dreamdictLangNames[lang]
|
||||||
|
if langName == "" {
|
||||||
|
langName = lang
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick a word, optionally with a translatable clue
|
||||||
|
var word, clueWord string
|
||||||
|
for attempt := 0; attempt < 3; attempt++ {
|
||||||
|
candidate, err := p.dict.RandomWord(lang, "", 4, 12)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("hangman: random word failed", "lang", lang, "err", err)
|
||||||
|
rollback()
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, "❌ Dictionary service unavailable. Try again shortly.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if clueLang == "" {
|
||||||
|
word = candidate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
translations, err := p.dict.Translate(candidate, lang, clueLang)
|
||||||
|
if err != nil || len(translations) == 0 {
|
||||||
|
continue // retry with a different word
|
||||||
|
}
|
||||||
|
word = candidate
|
||||||
|
clueWord = translations[0]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if word == "" {
|
||||||
|
// Fallback: start without a clue
|
||||||
|
candidate, err := p.dict.RandomWord(lang, "", 4, 12)
|
||||||
|
if err != nil {
|
||||||
|
rollback()
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, "❌ Dictionary service unavailable. Try again shortly.")
|
||||||
|
}
|
||||||
|
word = candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
game := newHangmanGame(word, p.maxWrong)
|
||||||
|
game.lang = lang
|
||||||
|
game.clueLang = clueLang
|
||||||
|
game.clueWord = clueWord
|
||||||
|
|
||||||
|
// Replace the placeholder reserved by handleStart
|
||||||
|
p.mu.Lock()
|
||||||
|
p.games[ctx.RoomID] = game
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
// Build thread root
|
||||||
|
var sb strings.Builder
|
||||||
|
if clueLang != "" {
|
||||||
|
clueLangName := dreamdictLangNames[clueLang]
|
||||||
|
if clueLangName == "" {
|
||||||
|
clueLangName = clueLang
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("🐝 **Hangman — %s** (clue: %s)\n", langName, clueLangName))
|
||||||
|
} else {
|
||||||
|
sb.WriteString(fmt.Sprintf("🐝 **Hangman — %s**\n", langName))
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("Word: %s (%d letters)\n", game.displayPhrase(), len([]rune(word))))
|
||||||
|
|
||||||
|
if clueWord != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf("💡 Clue: %s\n", clueWord))
|
||||||
|
} else if clueLang != "" {
|
||||||
|
sb.WriteString("💡 No clue available for this word.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString(fmt.Sprintf("\nWrong guesses: 0/%d\n", game.maxWrong))
|
||||||
|
sb.WriteString("Guess a letter: `!hangman <letter>`")
|
||||||
|
|
||||||
|
eventID, err := p.SendMessageID(ctx.RoomID, sb.String())
|
||||||
|
if err != nil {
|
||||||
|
p.mu.Lock()
|
||||||
|
delete(p.games, ctx.RoomID)
|
||||||
|
p.mu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mu.Lock()
|
||||||
|
game.threadID = eventID
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
// Materialize thread
|
||||||
|
return p.SendThread(ctx.RoomID, eventID, fmt.Sprintf(
|
||||||
|
"```\n%s\n```\n%s\n\nWrong guesses: none",
|
||||||
|
gallows[0], game.displayPhrase(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
func (p *HangmanPlugin) handleGuess(ctx MessageContext, guess string) error {
|
func (p *HangmanPlugin) handleGuess(ctx MessageContext, guess string) error {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
game, active := p.games[ctx.RoomID]
|
game, active := p.games[ctx.RoomID]
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gogobee/internal/db"
|
"gogobee/internal/db"
|
||||||
|
"gogobee/internal/dreamclient"
|
||||||
|
|
||||||
"maunium.net/go/mautrix"
|
"maunium.net/go/mautrix"
|
||||||
)
|
)
|
||||||
@@ -22,13 +22,15 @@ var httpClient = &http.Client{Timeout: 15 * time.Second}
|
|||||||
type LookupPlugin struct {
|
type LookupPlugin struct {
|
||||||
Base
|
Base
|
||||||
rateLimiter *RateLimitsPlugin
|
rateLimiter *RateLimitsPlugin
|
||||||
|
dict *dreamclient.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLookupPlugin creates a new lookup plugin.
|
// NewLookupPlugin creates a new lookup plugin.
|
||||||
func NewLookupPlugin(client *mautrix.Client, rateLimiter *RateLimitsPlugin) *LookupPlugin {
|
func NewLookupPlugin(client *mautrix.Client, rateLimiter *RateLimitsPlugin, dict *dreamclient.Client) *LookupPlugin {
|
||||||
return &LookupPlugin{
|
return &LookupPlugin{
|
||||||
Base: NewBase(client),
|
Base: NewBase(client),
|
||||||
rateLimiter: rateLimiter,
|
rateLimiter: rateLimiter,
|
||||||
|
dict: dict,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ func (p *LookupPlugin) Commands() []CommandDef {
|
|||||||
{Name: "wiki", Description: "Look up a Wikipedia summary", Usage: "!wiki <topic>", Category: "Lookup & Reference"},
|
{Name: "wiki", Description: "Look up a Wikipedia summary", Usage: "!wiki <topic>", Category: "Lookup & Reference"},
|
||||||
{Name: "define", Description: "Look up a word definition", Usage: "!define <word>", Category: "Lookup & Reference"},
|
{Name: "define", Description: "Look up a word definition", Usage: "!define <word>", Category: "Lookup & Reference"},
|
||||||
{Name: "urban", Description: "Look up Urban Dictionary definition", Usage: "!urban <term>", Category: "Lookup & Reference"},
|
{Name: "urban", Description: "Look up Urban Dictionary definition", Usage: "!urban <term>", Category: "Lookup & Reference"},
|
||||||
{Name: "translate", Description: "Translate text (pt/es/fr/de/ja/ko/zh/ar/ru/it)", Usage: "!translate [lang] <text>", Category: "Lookup & Reference"},
|
{Name: "translate", Description: "Look up word translations (en/fr/pt-PT)", Usage: "!translate <word> [lang]", Category: "Lookup & Reference"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,78 +285,114 @@ func (p *LookupPlugin) handleUrban(ctx MessageContext) error {
|
|||||||
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
var supportedLangs = map[string]bool{
|
// dreamdictLangs are the supported DreamDict languages in lookup order.
|
||||||
"pt": true, "es": true, "fr": true, "de": true, "ja": true,
|
var dreamdictLangs = []string{"en", "fr", "pt-PT"}
|
||||||
"ko": true, "zh": true, "ar": true, "ru": true, "it": true,
|
|
||||||
|
// dreamdictLangNames maps language tags to display names.
|
||||||
|
var dreamdictLangNames = map[string]string{
|
||||||
|
"en": "English",
|
||||||
|
"fr": "French",
|
||||||
|
"pt-PT": "Portuguese",
|
||||||
|
}
|
||||||
|
|
||||||
|
// normaliseLang normalises a language tag to the canonical DreamDict form.
|
||||||
|
func normaliseLang(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"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *LookupPlugin) handleTranslate(ctx MessageContext) error {
|
func (p *LookupPlugin) handleTranslate(ctx MessageContext) error {
|
||||||
args := strings.TrimSpace(p.GetArgs(ctx.Body, "translate"))
|
args := strings.TrimSpace(p.GetArgs(ctx.Body, "translate"))
|
||||||
if args == "" {
|
if args == "" {
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||||
"Usage: !translate [lang] <text>\nSupported: pt, es, fr, de, ja, ko, zh, ar, ru, it")
|
"Usage: !translate <word> [lang]\nSupported languages: en, fr, pt-PT")
|
||||||
}
|
}
|
||||||
|
|
||||||
ltURL := os.Getenv("LIBRETRANSLATE_URL")
|
if p.dict == nil {
|
||||||
if ltURL == "" {
|
return p.SendReply(ctx.RoomID, ctx.EventID, "❌ Dictionary service unavailable. Try again shortly.")
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Translation service is not configured.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rate limit (configurable, default 20/day to match TS version)
|
parts := strings.Fields(args)
|
||||||
translateLimit := 20
|
word := parts[0]
|
||||||
if v := os.Getenv("RATELIMIT_TRANSLATE"); v != "" {
|
var sourceLang string
|
||||||
if n, err := fmt.Sscanf(v, "%d", &translateLimit); n != 1 || err != nil {
|
|
||||||
translateLimit = 20
|
if len(parts) >= 2 {
|
||||||
}
|
sourceLang = normaliseLang(parts[1])
|
||||||
}
|
if sourceLang == "" {
|
||||||
if p.rateLimiter != nil && !p.rateLimiter.CheckLimit(ctx.Sender, "translate", translateLimit) {
|
|
||||||
remaining := p.rateLimiter.Remaining(ctx.Sender, "translate", translateLimit)
|
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||||
fmt.Sprintf("Translation rate limit reached. %d remaining today.", remaining))
|
fmt.Sprintf("❌ Unknown language \"%s\". Supported: en, fr, pt-PT", parts[1]))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse lang code and text
|
// If lang specified, validate word exists in that language
|
||||||
parts := strings.SplitN(args, " ", 2)
|
if sourceLang != "" {
|
||||||
targetLang := "es" // default
|
valid, err := p.dict.IsValidWord(word, sourceLang)
|
||||||
text := args
|
|
||||||
|
|
||||||
if len(parts) >= 2 && supportedLangs[strings.ToLower(parts[0])] {
|
|
||||||
targetLang = strings.ToLower(parts[0])
|
|
||||||
text = parts[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call LibreTranslate
|
|
||||||
payload := fmt.Sprintf(`{"q":%q,"source":"auto","target":%q}`, text, targetLang)
|
|
||||||
req, err := http.NewRequest("POST", strings.TrimRight(ltURL, "/")+"/translate", strings.NewReader(payload))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to create translation request.")
|
slog.Error("lookup: translate valid check", "err", err)
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, "❌ Dictionary service unavailable. Try again shortly.")
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
if !valid {
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||||
resp, err := httpClient.Do(req)
|
fmt.Sprintf("❌ \"%s\" not found in %s.", word, dreamdictLangNames[sourceLang]))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Auto-detect: try each language in order
|
||||||
|
for _, lang := range dreamdictLangs {
|
||||||
|
valid, err := p.dict.IsValidWord(word, lang)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("lookup: translate request", "err", err)
|
slog.Error("lookup: translate auto-detect", "lang", lang, "err", err)
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to reach translation service.")
|
return p.SendReply(ctx.RoomID, ctx.EventID, "❌ Dictionary service unavailable. Try again shortly.")
|
||||||
|
}
|
||||||
|
if valid {
|
||||||
|
sourceLang = lang
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sourceLang == "" {
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||||
|
fmt.Sprintf("❌ \"%s\" not found in any supported language.", word))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
// Translate to other languages
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(fmt.Sprintf("🔤 %s (%s)\n", word, sourceLang))
|
||||||
|
|
||||||
|
for _, targetLang := range dreamdictLangs {
|
||||||
|
if targetLang == sourceLang {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
translations, err := p.dict.Translate(word, sourceLang, targetLang)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to read translation response.")
|
slog.Error("lookup: translate call", "from", sourceLang, "to", targetLang, "err", err)
|
||||||
|
sb.WriteString(fmt.Sprintf(" → %s: (no direct translation)\n", targetLang))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(translations) == 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf(" → %s: (no direct translation)\n", targetLang))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
display := translations
|
||||||
|
extra := 0
|
||||||
|
if len(display) > 5 {
|
||||||
|
extra = len(display) - 5
|
||||||
|
display = display[:5]
|
||||||
|
}
|
||||||
|
line := strings.Join(display, ", ")
|
||||||
|
if extra > 0 {
|
||||||
|
line += fmt.Sprintf(" (+%d more)", extra)
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf(" → %s: %s\n", targetLang, line))
|
||||||
}
|
}
|
||||||
|
|
||||||
var result struct {
|
return p.SendReply(ctx.RoomID, ctx.EventID, strings.TrimRight(sb.String(), "\n"))
|
||||||
TranslatedText string `json:"translatedText"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(body, &result); err != nil {
|
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to parse translation response.")
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.Error != "" {
|
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("Translation error: %s", result.Error))
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := fmt.Sprintf("Translation (-> %s):\n%s", targetLang, result.TranslatedText)
|
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package plugin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -14,42 +13,23 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gogobee/internal/db"
|
"gogobee/internal/db"
|
||||||
|
"gogobee/internal/dreamclient"
|
||||||
|
|
||||||
"maunium.net/go/mautrix"
|
"maunium.net/go/mautrix"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// wordnikWOTDResponse is the top-level Wordnik Word of the Day response.
|
// WOTDPlugin provides a Word of the Day feature using DreamDict.
|
||||||
type wordnikWOTDResponse struct {
|
|
||||||
Word string `json:"word"`
|
|
||||||
Definitions []wordnikWOTDDef `json:"definitions"`
|
|
||||||
Examples []wordnikWOTDEx `json:"examples"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type wordnikWOTDDef struct {
|
|
||||||
Text string `json:"text"`
|
|
||||||
PartOfSpeech string `json:"partOfSpeech"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type wordnikWOTDEx struct {
|
|
||||||
Text string `json:"text"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// WOTDPlugin provides a Word of the Day feature using the Wordnik API.
|
|
||||||
type WOTDPlugin struct {
|
type WOTDPlugin struct {
|
||||||
Base
|
Base
|
||||||
apiKey string
|
dict *dreamclient.Client
|
||||||
httpClient *http.Client
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWOTDPlugin creates a new WOTDPlugin.
|
// NewWOTDPlugin creates a new WOTDPlugin.
|
||||||
func NewWOTDPlugin(client *mautrix.Client) *WOTDPlugin {
|
func NewWOTDPlugin(client *mautrix.Client, dict *dreamclient.Client) *WOTDPlugin {
|
||||||
return &WOTDPlugin{
|
return &WOTDPlugin{
|
||||||
Base: NewBase(client),
|
Base: NewBase(client),
|
||||||
apiKey: os.Getenv("WORDNIK_API_KEY"),
|
dict: dict,
|
||||||
httpClient: &http.Client{
|
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +37,7 @@ func (p *WOTDPlugin) Name() string { return "wotd" }
|
|||||||
|
|
||||||
func (p *WOTDPlugin) Commands() []CommandDef {
|
func (p *WOTDPlugin) Commands() []CommandDef {
|
||||||
return []CommandDef{
|
return []CommandDef{
|
||||||
{Name: "wotd", Description: "Show today's Word of the Day", Usage: "!wotd", Category: "Lookup & Reference"},
|
{Name: "wotd", Description: "Show today's Palavra do Dia", Usage: "!wotd [force]", Category: "Lookup & Reference"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,9 +48,16 @@ func (p *WOTDPlugin) OnReaction(_ ReactionContext) error { return nil }
|
|||||||
func (p *WOTDPlugin) OnMessage(ctx MessageContext) error {
|
func (p *WOTDPlugin) OnMessage(ctx MessageContext) error {
|
||||||
if p.IsCommand(ctx.Body, "wotd") {
|
if p.IsCommand(ctx.Body, "wotd") {
|
||||||
go func() {
|
go func() {
|
||||||
|
args := strings.TrimSpace(p.GetArgs(ctx.Body, "wotd"))
|
||||||
|
if strings.ToLower(args) == "force" {
|
||||||
|
if err := p.handleWOTDForce(ctx); err != nil {
|
||||||
|
slog.Error("wotd: force handler error", "err", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if err := p.handleWOTD(ctx); err != nil {
|
if err := p.handleWOTD(ctx); err != nil {
|
||||||
slog.Error("wotd: handler error", "err", err)
|
slog.Error("wotd: handler error", "err", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -83,68 +70,96 @@ func (p *WOTDPlugin) OnMessage(ctx MessageContext) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefetch fetches today's Word of the Day from Wordnik and stores it in the database.
|
// Prefetch picks today's Palavra do Dia from DreamDict and stores it in the database.
|
||||||
|
// Prefers pt-PT words with at least one definition and one English translation.
|
||||||
func (p *WOTDPlugin) Prefetch() error {
|
func (p *WOTDPlugin) Prefetch() error {
|
||||||
if p.apiKey == "" {
|
return p.prefetchWord(false)
|
||||||
slog.Warn("wotd: WORDNIK_API_KEY not set, skipping prefetch")
|
}
|
||||||
|
|
||||||
|
func (p *WOTDPlugin) prefetchWord(force bool) error {
|
||||||
|
if p.dict == nil {
|
||||||
|
slog.Warn("wotd: DreamDict not configured, skipping prefetch")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
today := time.Now().UTC().Format("2006-01-02")
|
today := time.Now().UTC().Format("2006-01-02")
|
||||||
|
|
||||||
// Check if already fetched
|
|
||||||
d := db.Get()
|
d := db.Get()
|
||||||
|
|
||||||
|
if !force {
|
||||||
var exists int
|
var exists int
|
||||||
err := d.QueryRow(`SELECT 1 FROM wotd_log WHERE date = ?`, today).Scan(&exists)
|
err := d.QueryRow(`SELECT 1 FROM wotd_log WHERE date = ?`, today).Scan(&exists)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
slog.Info("wotd: already fetched for today", "date", today)
|
slog.Info("wotd: already fetched for today", "date", today)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
url := fmt.Sprintf("https://api.wordnik.com/v4/words.json/wordOfTheDay?api_key=%s", p.apiKey)
|
// Pick a random pt-PT word with definitions; prefer one with English translations.
|
||||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
|
var word, definition, partOfSpeech, translationsJSON string
|
||||||
|
|
||||||
|
for attempt := 0; attempt < 10; attempt++ {
|
||||||
|
candidate, err := p.dict.RandomWord("pt-PT", "", 4, 14)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wotd: create request: %w", err)
|
slog.Warn("wotd: random word attempt failed", "attempt", attempt+1, "err", err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := p.httpClient.Do(req)
|
defs, err := p.dict.Define(candidate, "pt-PT")
|
||||||
if err != nil {
|
if err != nil || len(defs) == 0 {
|
||||||
return fmt.Errorf("wotd: fetch: %w", err)
|
continue
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("wotd: API returned status %d", resp.StatusCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var wotd wordnikWOTDResponse
|
// Check for English translation (preferred but not required after 5 attempts)
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&wotd); err != nil {
|
enTrans, _ := p.dict.Translate(candidate, "pt-PT", "en")
|
||||||
return fmt.Errorf("wotd: decode response: %w", err)
|
if len(enTrans) == 0 && attempt < 5 {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
definition := ""
|
frTrans, _ := p.dict.Translate(candidate, "pt-PT", "fr")
|
||||||
partOfSpeech := ""
|
|
||||||
if len(wotd.Definitions) > 0 {
|
word = candidate
|
||||||
definition = wotd.Definitions[0].Text
|
definition = defs[0].Gloss
|
||||||
partOfSpeech = wotd.Definitions[0].PartOfSpeech
|
partOfSpeech = defs[0].POS
|
||||||
|
|
||||||
|
// Store translations as JSON in the example column
|
||||||
|
transMap := map[string][]string{}
|
||||||
|
if len(enTrans) > 0 {
|
||||||
|
transMap["en"] = enTrans
|
||||||
|
}
|
||||||
|
if len(frTrans) > 0 {
|
||||||
|
transMap["fr"] = frTrans
|
||||||
|
}
|
||||||
|
if data, err := json.Marshal(transMap); err == nil {
|
||||||
|
translationsJSON = string(data)
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
example := ""
|
if word == "" {
|
||||||
if len(wotd.Examples) > 0 {
|
return fmt.Errorf("wotd: failed to find a pt-PT word with definitions after 10 attempts")
|
||||||
example = wotd.Examples[0].Text
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = d.Exec(
|
if force {
|
||||||
|
// Delete existing entry for today so the INSERT below replaces it
|
||||||
|
if _, delErr := d.Exec(`DELETE FROM wotd_log WHERE date = ?`, today); delErr != nil {
|
||||||
|
slog.Error("wotd: force delete failed", "err", delErr)
|
||||||
|
}
|
||||||
|
// Also clear job-completed flags so PostWOTD will re-post
|
||||||
|
d.Exec(`DELETE FROM job_completed WHERE job_name = 'wotd' AND job_key LIKE ?`, today+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := d.Exec(
|
||||||
`INSERT INTO wotd_log (date, word, definition, part_of_speech, example, posted)
|
`INSERT INTO wotd_log (date, word, definition, part_of_speech, example, posted)
|
||||||
VALUES (?, ?, ?, ?, ?, 0)
|
VALUES (?, ?, ?, ?, ?, 0)
|
||||||
ON CONFLICT(date) DO NOTHING`,
|
ON CONFLICT(date) DO UPDATE SET word = ?, definition = ?, part_of_speech = ?, example = ?, posted = 0`,
|
||||||
today, wotd.Word, definition, partOfSpeech, example,
|
today, word, definition, partOfSpeech, translationsJSON,
|
||||||
|
word, definition, partOfSpeech, translationsJSON,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wotd: store: %w", err)
|
return fmt.Errorf("wotd: store: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("wotd: prefetched", "date", today, "word", wotd.Word)
|
slog.Info("wotd: prefetched", "date", today, "word", word)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,26 +231,64 @@ func (p *WOTDPlugin) handleWOTD(ctx MessageContext) error {
|
|||||||
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *WOTDPlugin) formatWOTD(word, definition, partOfSpeech, example string) string {
|
func (p *WOTDPlugin) formatWOTD(word, definition, partOfSpeech, translationsJSON string) string {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
dateStr := now.Format("Monday, 2 January 2006")
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString(fmt.Sprintf("Word of the Day: %s", word))
|
sb.WriteString(fmt.Sprintf("📖 **Palavra do Dia** — %s\n\n", dateStr))
|
||||||
|
|
||||||
if partOfSpeech != "" {
|
if partOfSpeech != "" {
|
||||||
sb.WriteString(fmt.Sprintf(" (%s)", partOfSpeech))
|
sb.WriteString(fmt.Sprintf("✨ **%s** (%s)\n\n", word, partOfSpeech))
|
||||||
|
} else {
|
||||||
|
sb.WriteString(fmt.Sprintf("✨ **%s**\n\n", word))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Portuguese definition
|
||||||
if definition != "" {
|
if definition != "" {
|
||||||
sb.WriteString(fmt.Sprintf("\n\nDefinition: %s", definition))
|
sb.WriteString(fmt.Sprintf("🇵🇹 pt-PT\n %s\n\n", definition))
|
||||||
}
|
}
|
||||||
|
|
||||||
if example != "" {
|
// Parse translations from JSON stored in example column
|
||||||
sb.WriteString(fmt.Sprintf("\n\nExample: \"%s\"", example))
|
if translationsJSON != "" {
|
||||||
|
var transMap map[string][]string
|
||||||
|
if json.Unmarshal([]byte(translationsJSON), &transMap) == nil {
|
||||||
|
if enTrans, ok := transMap["en"]; ok && len(enTrans) > 0 {
|
||||||
|
display := enTrans
|
||||||
|
if len(display) > 5 {
|
||||||
|
display = display[:5]
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("🇬🇧 en\n %s\n\n", strings.Join(display, ", ")))
|
||||||
|
}
|
||||||
|
if frTrans, ok := transMap["fr"]; ok && len(frTrans) > 0 {
|
||||||
|
display := frTrans
|
||||||
|
if len(display) > 5 {
|
||||||
|
display = display[:5]
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("🇫🇷 fr\n %s\n\n", strings.Join(display, ", ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.WriteString("\n\nUse this word in a message today to earn 25 XP!")
|
sb.WriteString("━━━━━━━━━━━━━━━━━━━━\n")
|
||||||
|
sb.WriteString(fmt.Sprintf("Learn more: `!define %s pt-PT`\n", word))
|
||||||
|
sb.WriteString("Use this word in a message today to earn 25 XP!")
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *WOTDPlugin) handleWOTDForce(ctx MessageContext) error {
|
||||||
|
if !p.IsAdmin(ctx.Sender) {
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Only moderators can force a new Word of the Day.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.prefetchWord(true); err != nil {
|
||||||
|
slog.Error("wotd: force prefetch failed", "err", err)
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to fetch a new Word of the Day. Try again later.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.handleWOTD(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// trackUsage checks if the user used the WOTD in their message and rewards them.
|
// trackUsage checks if the user used the WOTD in their message and rewards them.
|
||||||
func (p *WOTDPlugin) trackUsage(ctx MessageContext) {
|
func (p *WOTDPlugin) trackUsage(ctx MessageContext) {
|
||||||
today := time.Now().UTC().Format("2006-01-02")
|
today := time.Now().UTC().Format("2006-01-02")
|
||||||
|
|||||||
22
main.go
22
main.go
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"gogobee/internal/bot"
|
"gogobee/internal/bot"
|
||||||
"gogobee/internal/db"
|
"gogobee/internal/db"
|
||||||
|
"gogobee/internal/dreamclient"
|
||||||
"gogobee/internal/plugin"
|
"gogobee/internal/plugin"
|
||||||
"gogobee/internal/util"
|
"gogobee/internal/util"
|
||||||
|
|
||||||
@@ -95,9 +96,22 @@ func main() {
|
|||||||
registry.Register(plugin.NewToolsPlugin(client))
|
registry.Register(plugin.NewToolsPlugin(client))
|
||||||
registry.Register(plugin.NewUserPlugin(client))
|
registry.Register(plugin.NewUserPlugin(client))
|
||||||
|
|
||||||
|
// Dictionary service
|
||||||
|
var dictClient *dreamclient.Client
|
||||||
|
if dictURL := os.Getenv("DREAMDICT_URL"); dictURL != "" {
|
||||||
|
dictClient = dreamclient.New(dictURL)
|
||||||
|
if h, err := dictClient.Health(); err != nil {
|
||||||
|
slog.Warn("dreamdict not reachable — dictionary features degraded", "url", dictURL, "err", err)
|
||||||
|
} else {
|
||||||
|
slog.Info("dreamdict connected", "url", dictURL, "words_en", h.WordCounts["en"])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
slog.Warn("DREAMDICT_URL not set — dictionary features disabled")
|
||||||
|
}
|
||||||
|
|
||||||
// Entertainment / Lookup
|
// Entertainment / Lookup
|
||||||
registry.Register(plugin.NewRetroPlugin(client))
|
registry.Register(plugin.NewRetroPlugin(client))
|
||||||
registry.Register(plugin.NewLookupPlugin(client, ratePlugin))
|
registry.Register(plugin.NewLookupPlugin(client, ratePlugin, dictClient))
|
||||||
registry.Register(plugin.NewCountdownPlugin(client))
|
registry.Register(plugin.NewCountdownPlugin(client))
|
||||||
registry.Register(plugin.NewStocksPlugin(client))
|
registry.Register(plugin.NewStocksPlugin(client))
|
||||||
forexPlugin := plugin.NewForexPlugin(client)
|
forexPlugin := plugin.NewForexPlugin(client)
|
||||||
@@ -113,13 +127,13 @@ func main() {
|
|||||||
euroPlugin := plugin.NewEuroPlugin(client)
|
euroPlugin := plugin.NewEuroPlugin(client)
|
||||||
registry.Register(euroPlugin)
|
registry.Register(euroPlugin)
|
||||||
registry.Register(plugin.NewFlipPlugin(client))
|
registry.Register(plugin.NewFlipPlugin(client))
|
||||||
registry.Register(plugin.NewHangmanPlugin(client, euroPlugin))
|
registry.Register(plugin.NewHangmanPlugin(client, euroPlugin, dictClient))
|
||||||
registry.Register(plugin.NewBlackjackPlugin(client, euroPlugin))
|
registry.Register(plugin.NewBlackjackPlugin(client, euroPlugin))
|
||||||
registry.Register(plugin.NewUnoPlugin(client, euroPlugin))
|
registry.Register(plugin.NewUnoPlugin(client, euroPlugin))
|
||||||
registry.Register(plugin.NewHoldemPlugin(client, euroPlugin))
|
registry.Register(plugin.NewHoldemPlugin(client, euroPlugin))
|
||||||
adventurePlugin := plugin.NewAdventurePlugin(client, euroPlugin)
|
adventurePlugin := plugin.NewAdventurePlugin(client, euroPlugin)
|
||||||
registry.Register(adventurePlugin)
|
registry.Register(adventurePlugin)
|
||||||
wordlePlugin := plugin.NewWordlePlugin(client, euroPlugin)
|
wordlePlugin := plugin.NewWordlePlugin(client, euroPlugin, dictClient)
|
||||||
registry.Register(wordlePlugin)
|
registry.Register(wordlePlugin)
|
||||||
|
|
||||||
// Community
|
// Community
|
||||||
@@ -144,7 +158,7 @@ func main() {
|
|||||||
registry.Register(plugin.NewLLMPassivePlugin(client, xpPlugin))
|
registry.Register(plugin.NewLLMPassivePlugin(client, xpPlugin))
|
||||||
|
|
||||||
// Scheduled
|
// Scheduled
|
||||||
wotdPlugin := plugin.NewWOTDPlugin(client)
|
wotdPlugin := plugin.NewWOTDPlugin(client, dictClient)
|
||||||
registry.Register(wotdPlugin)
|
registry.Register(wotdPlugin)
|
||||||
holidaysPlugin := plugin.NewHolidaysPlugin(client)
|
holidaysPlugin := plugin.NewHolidaysPlugin(client)
|
||||||
registry.Register(holidaysPlugin)
|
registry.Register(holidaysPlugin)
|
||||||
|
|||||||
Reference in New Issue
Block a user