mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 00:52:40 +00:00
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>
This commit is contained in:
206
internal/plugin/welcome.go
Normal file
206
internal/plugin/welcome.go
Normal file
@@ -0,0 +1,206 @@
|
||||
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!")
|
||||
}
|
||||
Reference in New Issue
Block a user