Files
gogobee/internal/plugin/welcome.go
prosolis ddb196aaad Rewrite from TypeScript to Go
Complete rewrite of the Freebee Matrix bot as GogoBee using mautrix-go.

- E2EE with goolm (pure Go, no CGo/libolm) and cross-signing bootstrap
- 35+ plugins with dependency injection and ordered registration
- SQLite storage via modernc.org/sqlite (no CGo)
- Scheduler via robfig/cron for WOTD, holidays, birthdays, releases, etc.
- Optional LLM integration (Ollama) for sentiment, profanity, roasts, vibes
- Threaded trivia, three-tier profanity tracking, WOTD usage verification
- Multi-country holiday support with deduplication
- Quadratic XP curve, configurable bot display name, encrypted DMs

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

207 lines
5.1 KiB
Go
Raw 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",
"Reactions",
"Info",
}
// categoryEmojis maps categories to display emojis.
var categoryEmojis = map[string]string{
"Fun & Games": "🎲",
"Leveling & Stats": "📊",
"Entertainment": "🎬",
"Lookup & Reference": "📖",
"LLM & Sentiment": "🧠",
"Personal": "👤",
"Holidays": "🎉",
"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!")
}