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!") }