mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 16:42:41 +00:00
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>
237 lines
6.6 KiB
Go
237 lines
6.6 KiB
Go
package plugin
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
"time"
|
|
|
|
"gogobee/internal/db"
|
|
|
|
"maunium.net/go/mautrix"
|
|
"maunium.net/go/mautrix/id"
|
|
)
|
|
|
|
// PresencePlugin handles away status and user profile lookups.
|
|
type PresencePlugin struct {
|
|
Base
|
|
}
|
|
|
|
// NewPresencePlugin creates a new PresencePlugin.
|
|
func NewPresencePlugin(client *mautrix.Client) *PresencePlugin {
|
|
return &PresencePlugin{
|
|
Base: NewBase(client),
|
|
}
|
|
}
|
|
|
|
func (p *PresencePlugin) Name() string { return "presence" }
|
|
|
|
func (p *PresencePlugin) Commands() []CommandDef {
|
|
return []CommandDef{
|
|
{Name: "away", Description: "Set away status", Usage: "!away [message]", Category: "Personal"},
|
|
{Name: "afk", Description: "Set away status (alias)", Usage: "!afk [message]", Category: "Personal"},
|
|
{Name: "back", Description: "Clear away status", Usage: "!back", Category: "Personal"},
|
|
{Name: "whois", Description: "Show user profile card", Usage: "!whois @user", Category: "Personal"},
|
|
}
|
|
}
|
|
|
|
func (p *PresencePlugin) Init() error { return nil }
|
|
|
|
func (p *PresencePlugin) OnReaction(_ ReactionContext) error { return nil }
|
|
|
|
func (p *PresencePlugin) OnMessage(ctx MessageContext) error {
|
|
switch {
|
|
case p.IsCommand(ctx.Body, "away"):
|
|
return p.handleAway(ctx, p.GetArgs(ctx.Body, "away"))
|
|
case p.IsCommand(ctx.Body, "afk"):
|
|
return p.handleAway(ctx, p.GetArgs(ctx.Body, "afk"))
|
|
case p.IsCommand(ctx.Body, "back"):
|
|
return p.handleBack(ctx)
|
|
case p.IsCommand(ctx.Body, "whois"):
|
|
return p.handleWhois(ctx)
|
|
default:
|
|
// Auto-clear away status on non-command messages
|
|
return p.autoClearAway(ctx)
|
|
}
|
|
}
|
|
|
|
func (p *PresencePlugin) handleAway(ctx MessageContext, message string) error {
|
|
if message == "" {
|
|
message = "Away"
|
|
}
|
|
|
|
now := time.Now().Unix()
|
|
_, err := db.Get().Exec(
|
|
`INSERT INTO presence (user_id, status, message, updated_at)
|
|
VALUES (?, 'away', ?, ?)
|
|
ON CONFLICT(user_id) DO UPDATE SET
|
|
status = 'away',
|
|
message = ?,
|
|
updated_at = ?`,
|
|
string(ctx.Sender), message, now, message, now,
|
|
)
|
|
if err != nil {
|
|
slog.Error("presence: set away", "err", err)
|
|
return p.SendMessage(ctx.RoomID, "Failed to set away status.")
|
|
}
|
|
|
|
return p.SendReply(ctx.RoomID, ctx.EventID,
|
|
fmt.Sprintf("💤 %s is now away: %s", ctx.Sender, message))
|
|
}
|
|
|
|
func (p *PresencePlugin) handleBack(ctx MessageContext) error {
|
|
result, err := db.Get().Exec(
|
|
`UPDATE presence SET status = 'online', message = '', updated_at = ?
|
|
WHERE user_id = ? AND status = 'away'`,
|
|
time.Now().Unix(), string(ctx.Sender),
|
|
)
|
|
if err != nil {
|
|
slog.Error("presence: set back", "err", err)
|
|
return p.SendMessage(ctx.RoomID, "Failed to clear away status.")
|
|
}
|
|
|
|
affected, _ := result.RowsAffected()
|
|
if affected == 0 {
|
|
return p.SendMessage(ctx.RoomID, "You weren't marked as away.")
|
|
}
|
|
|
|
return p.SendReply(ctx.RoomID, ctx.EventID,
|
|
fmt.Sprintf("👋 Welcome back, %s!", ctx.Sender))
|
|
}
|
|
|
|
func (p *PresencePlugin) autoClearAway(ctx MessageContext) error {
|
|
// Check if user is away
|
|
var status string
|
|
err := db.Get().QueryRow(
|
|
`SELECT status FROM presence WHERE user_id = ?`,
|
|
string(ctx.Sender),
|
|
).Scan(&status)
|
|
|
|
if err != nil || status != "away" {
|
|
return nil
|
|
}
|
|
|
|
// Clear away status
|
|
_, err = db.Get().Exec(
|
|
`UPDATE presence SET status = 'online', message = '', updated_at = ?
|
|
WHERE user_id = ?`,
|
|
time.Now().Unix(), string(ctx.Sender),
|
|
)
|
|
if err != nil {
|
|
slog.Error("presence: auto-clear away", "err", err)
|
|
return nil
|
|
}
|
|
|
|
return p.SendMessage(ctx.RoomID, fmt.Sprintf("👋 Welcome back, %s! (auto-cleared away status)", ctx.Sender))
|
|
}
|
|
|
|
func (p *PresencePlugin) handleWhois(ctx MessageContext) error {
|
|
args := strings.TrimSpace(p.GetArgs(ctx.Body, "whois"))
|
|
if args == "" {
|
|
return p.SendMessage(ctx.RoomID, "Usage: !whois @user:server")
|
|
}
|
|
|
|
targetUser := id.UserID(args)
|
|
|
|
// Gather profile data
|
|
var displayName string
|
|
var xp, level int
|
|
err := db.Get().QueryRow(
|
|
`SELECT display_name, xp, level FROM users WHERE user_id = ?`,
|
|
string(targetUser),
|
|
).Scan(&displayName, &xp, &level)
|
|
if err != nil {
|
|
displayName = string(targetUser)
|
|
}
|
|
|
|
// Get stats
|
|
var totalMessages, totalWords int
|
|
_ = db.Get().QueryRow(
|
|
`SELECT total_messages, total_words FROM user_stats WHERE user_id = ?`,
|
|
string(targetUser),
|
|
).Scan(&totalMessages, &totalWords)
|
|
|
|
// Get streak
|
|
var currentStreak int
|
|
today := time.Now().Format("2006-01-02")
|
|
yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02")
|
|
var countToday, countYesterday int
|
|
_ = db.Get().QueryRow(
|
|
`SELECT COUNT(*) FROM daily_activity WHERE user_id = ? AND date = ?`,
|
|
string(targetUser), today,
|
|
).Scan(&countToday)
|
|
_ = db.Get().QueryRow(
|
|
`SELECT COUNT(*) FROM daily_activity WHERE user_id = ? AND date = ?`,
|
|
string(targetUser), yesterday,
|
|
).Scan(&countYesterday)
|
|
if countToday > 0 || countYesterday > 0 {
|
|
// Count consecutive days backward
|
|
currentStreak = 0
|
|
checkDate := time.Now()
|
|
for {
|
|
dateStr := checkDate.Format("2006-01-02")
|
|
var cnt int
|
|
err := db.Get().QueryRow(
|
|
`SELECT COUNT(*) FROM daily_activity WHERE user_id = ? AND date = ?`,
|
|
string(targetUser), dateStr,
|
|
).Scan(&cnt)
|
|
if err != nil || cnt == 0 {
|
|
break
|
|
}
|
|
currentStreak++
|
|
checkDate = checkDate.AddDate(0, 0, -1)
|
|
}
|
|
}
|
|
|
|
// Get reputation (from XP log with reason = 'rep')
|
|
var repCount int
|
|
_ = db.Get().QueryRow(
|
|
`SELECT COUNT(*) FROM xp_log WHERE user_id = ? AND reason = 'rep'`,
|
|
string(targetUser),
|
|
).Scan(&repCount)
|
|
|
|
// Get presence status
|
|
var status, statusMsg string
|
|
_ = db.Get().QueryRow(
|
|
`SELECT status, message FROM presence WHERE user_id = ?`,
|
|
string(targetUser),
|
|
).Scan(&status, &statusMsg)
|
|
|
|
// Get birthday
|
|
var bdayMonth, bdayDay int
|
|
var timezone string
|
|
hasBirthday := false
|
|
err = db.Get().QueryRow(
|
|
`SELECT month, day, timezone FROM birthdays WHERE user_id = ?`,
|
|
string(targetUser),
|
|
).Scan(&bdayMonth, &bdayDay, &timezone)
|
|
if err == nil {
|
|
hasBirthday = true
|
|
}
|
|
if timezone == "" {
|
|
timezone = "Not set"
|
|
}
|
|
|
|
// Build profile card
|
|
var sb strings.Builder
|
|
sb.WriteString(fmt.Sprintf("👤 **Profile: %s**\n", displayName))
|
|
sb.WriteString(fmt.Sprintf(" ID: %s\n", targetUser))
|
|
sb.WriteString(fmt.Sprintf(" Level: %d (XP: %d)\n", level, xp))
|
|
sb.WriteString(fmt.Sprintf(" Messages: %d | Words: %d\n", totalMessages, totalWords))
|
|
sb.WriteString(fmt.Sprintf(" Rep: +%d\n", repCount))
|
|
sb.WriteString(fmt.Sprintf(" Streak: %d days\n", currentStreak))
|
|
sb.WriteString(fmt.Sprintf(" Timezone: %s\n", timezone))
|
|
|
|
if hasBirthday {
|
|
sb.WriteString(fmt.Sprintf(" Birthday: %s %d\n", time.Month(bdayMonth).String(), bdayDay))
|
|
}
|
|
|
|
if status == "away" {
|
|
sb.WriteString(fmt.Sprintf(" Status: 💤 Away — %s\n", statusMsg))
|
|
} else {
|
|
sb.WriteString(" Status: 🟢 Online\n")
|
|
}
|
|
|
|
return p.SendMessage(ctx.RoomID, sb.String())
|
|
}
|