mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +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:
236
internal/plugin/presence.go
Normal file
236
internal/plugin/presence.go
Normal file
@@ -0,0 +1,236 @@
|
||||
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())
|
||||
}
|
||||
Reference in New Issue
Block a user