Files
gogobee/internal/plugin/welcome.go
prosolis 0fc15668da Add space groups, fix HLTB scraper, fix quote wall E2EE, improve tarot prompts
Space groups: automatic room grouping via membership overlap percentage.
Rooms with sufficient shared members are grouped together so leaderboards,
trivia scores, and stats aggregate across the community. Uses strict
clique-based algorithm (every room must meet threshold with every other
room in group). Configurable via SPACE_GROUP_THRESHOLD env var. Persisted
to SQLite, recomputed hourly and on startup.

HLTB scraper: rewrote for new howlongtobeat.com API (two-step token auth
via /api/finder/init + /api/finder endpoint).

Quote wall: fix E2EE decrypt flow for reply-to-save (ParseRaw before
Decrypt, skip ParseRaw after Decrypt returns pre-parsed event). Fix
subcommand matching for delete/search. Remove broken star-reaction handler
and old quote handler from user.go.

Other fixes:
- Strip Matrix reply fallback before command detection (main.go)
- Increase Ollama context window to 8192
- Improve tarot spread prompts (~4x longer, narrative arc)
- Add "cards are props" instruction to tarot LLM prompt
- Fix movies.go CommandDef name mismatch
- Add Community category to !help
- Remove daily horoscope broadcast cron
- Update README with all changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 00:45:25 -07:00

209 lines
5.1 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package plugin
import (
"database/sql"
"fmt"
"log/slog"
"os"
"strings"
"gogobee/internal/db"
"maunium.net/go/mautrix"
)
// WelcomeRegistry is the interface for retrieving all registered commands (for !help).
type WelcomeRegistry interface {
GetCommands() []CommandDef
}
// WelcomePlugin detects new users and provides the !help command.
type WelcomePlugin struct {
Base
xp *XPPlugin
registry WelcomeRegistry
}
// NewWelcomePlugin creates a new welcome plugin.
func NewWelcomePlugin(client *mautrix.Client, xp *XPPlugin, registry WelcomeRegistry) *WelcomePlugin {
return &WelcomePlugin{
Base: NewBase(client),
xp: xp,
registry: registry,
}
}
func (p *WelcomePlugin) Name() string { return "welcome" }
func (p *WelcomePlugin) Commands() []CommandDef {
return []CommandDef{
{Name: "help", Description: "DM you a list of all bot commands", Usage: "!help", Category: "Info"},
}
}
func (p *WelcomePlugin) Init() error { return nil }
func (p *WelcomePlugin) OnReaction(_ ReactionContext) error { return nil }
func (p *WelcomePlugin) OnMessage(ctx MessageContext) error {
if p.IsCommand(ctx.Body, "help") {
return p.handleHelp(ctx)
}
// Passive: detect new users by checking for "welcome_wagon" achievement
p.checkNewUser(ctx)
return nil
}
func (p *WelcomePlugin) checkNewUser(ctx MessageContext) {
d := db.Get()
var exists int
err := d.QueryRow(
`SELECT 1 FROM achievements WHERE user_id = ? AND achievement_id = 'welcome_wagon'`,
string(ctx.Sender),
).Scan(&exists)
if err == nil {
// Already has the achievement, not a new user
return
}
if err != sql.ErrNoRows {
slog.Error("welcome: check achievement", "err", err)
return
}
// New user! Grant achievement
_, err = d.Exec(
`INSERT OR IGNORE INTO achievements (user_id, achievement_id) VALUES (?, 'welcome_wagon')`,
string(ctx.Sender),
)
if err != nil {
slog.Error("welcome: grant achievement", "err", err)
return
}
// Grant 25 XP
if p.xp != nil {
p.xp.GrantXP(ctx.Sender, 25, "welcome")
}
// Send welcome message
botName := os.Getenv("BOT_DISPLAY_NAME")
if botName == "" {
botName = "GogoBee"
}
welcome := fmt.Sprintf(
"Welcome to the community, %s! I'm %s, your friendly community bot.\n\n"+
"Here are some things you can do:\n"+
" - !help — see all available commands\n"+
" - !rank — check your XP and level\n"+
" - !streak — see your activity streak\n"+
" - !trivia — play trivia games\n\n"+
"You've been awarded 25 XP as a welcome gift! Have fun!",
string(ctx.Sender), botName,
)
if err := p.SendMessage(ctx.RoomID, welcome); err != nil {
slog.Error("welcome: send message", "err", err)
}
}
// categoryOrder defines the display order for help categories.
var categoryOrder = []string{
"Fun & Games",
"Leveling & Stats",
"Entertainment",
"Lookup & Reference",
"LLM & Sentiment",
"Personal",
"Holidays",
"Community",
"Reactions",
"Info",
}
// categoryEmojis maps categories to display emojis.
var categoryEmojis = map[string]string{
"Fun & Games": "🎲",
"Leveling & Stats": "📊",
"Entertainment": "🎬",
"Lookup & Reference": "📖",
"LLM & Sentiment": "🧠",
"Personal": "👤",
"Holidays": "🎉",
"Community": "💬",
"Reactions": "😎",
"Info": "",
"Admin": "🔧",
}
func (p *WelcomePlugin) handleHelp(ctx MessageContext) error {
cmds := p.registry.GetCommands()
if len(cmds) == 0 {
return p.SendReply(ctx.RoomID, ctx.EventID, "No commands available.")
}
// Group by category
grouped := make(map[string][]CommandDef)
var adminCmds []CommandDef
for _, cmd := range cmds {
if cmd.AdminOnly {
adminCmds = append(adminCmds, cmd)
continue
}
cat := cmd.Category
if cat == "" {
cat = "Other"
}
grouped[cat] = append(grouped[cat], cmd)
}
var sb strings.Builder
helpBotName := os.Getenv("BOT_DISPLAY_NAME")
if helpBotName == "" {
helpBotName = "GogoBee"
}
sb.WriteString(fmt.Sprintf("%s Commands\n", helpBotName))
for _, cat := range categoryOrder {
cmds, ok := grouped[cat]
if !ok || len(cmds) == 0 {
continue
}
emoji := categoryEmojis[cat]
sb.WriteString(fmt.Sprintf("\n%s %s\n", emoji, cat))
for _, cmd := range cmds {
sb.WriteString(fmt.Sprintf(" %s — %s\n", cmd.Usage, cmd.Description))
}
delete(grouped, cat)
}
// Any uncategorized leftovers
for cat, cmds := range grouped {
if len(cmds) == 0 {
continue
}
sb.WriteString(fmt.Sprintf("\n%s\n", cat))
for _, cmd := range cmds {
sb.WriteString(fmt.Sprintf(" %s — %s\n", cmd.Usage, cmd.Description))
}
}
if p.IsAdmin(ctx.Sender) && len(adminCmds) > 0 {
emoji := categoryEmojis["Admin"]
sb.WriteString(fmt.Sprintf("\n%s Admin\n", emoji))
for _, cmd := range adminCmds {
sb.WriteString(fmt.Sprintf(" %s — %s\n", cmd.Usage, cmd.Description))
}
}
// DM the help message
if err := p.SendDM(ctx.Sender, sb.String()); err != nil {
slog.Error("welcome: send help DM", "err", err)
return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to send help DM. Do you have DMs enabled?")
}
return p.SendReply(ctx.RoomID, ctx.EventID, "Help sent to your DMs!")
}