mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 00:32:40 +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>
222 lines
5.9 KiB
Go
222 lines
5.9 KiB
Go
package plugin
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"maunium.net/go/mautrix"
|
|
"maunium.net/go/mautrix/id"
|
|
)
|
|
|
|
const (
|
|
vibeBufferSize = 50
|
|
vibeCooldownMins = 5
|
|
vibeMinMessages = 10
|
|
)
|
|
|
|
type bufferedMessage struct {
|
|
Sender string
|
|
Body string
|
|
Time time.Time
|
|
}
|
|
|
|
// VibePlugin uses LLM to describe room energy or summarize conversation.
|
|
type VibePlugin struct {
|
|
Base
|
|
mu sync.Mutex
|
|
buffers map[id.RoomID][]bufferedMessage
|
|
cooldowns map[id.RoomID]time.Time
|
|
}
|
|
|
|
// NewVibePlugin creates a new vibe plugin.
|
|
func NewVibePlugin(client *mautrix.Client) *VibePlugin {
|
|
return &VibePlugin{
|
|
Base: NewBase(client),
|
|
buffers: make(map[id.RoomID][]bufferedMessage),
|
|
cooldowns: make(map[id.RoomID]time.Time),
|
|
}
|
|
}
|
|
|
|
func (p *VibePlugin) Name() string { return "vibe" }
|
|
|
|
func (p *VibePlugin) Commands() []CommandDef {
|
|
return []CommandDef{
|
|
{Name: "vibe", Description: "LLM describes the current room energy", Usage: "!vibe", Category: "LLM & Sentiment"},
|
|
{Name: "tldr", Description: "LLM summarizes recent conversation", Usage: "!tldr", Category: "LLM & Sentiment"},
|
|
}
|
|
}
|
|
|
|
func (p *VibePlugin) Init() error { return nil }
|
|
|
|
func (p *VibePlugin) OnReaction(_ ReactionContext) error { return nil }
|
|
|
|
func (p *VibePlugin) OnMessage(ctx MessageContext) error {
|
|
// Always buffer messages (except commands)
|
|
if !ctx.IsCommand {
|
|
p.addToBuffer(ctx)
|
|
}
|
|
|
|
if p.IsCommand(ctx.Body, "vibe") {
|
|
return p.handleVibe(ctx)
|
|
}
|
|
if p.IsCommand(ctx.Body, "tldr") {
|
|
return p.handleTLDR(ctx)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *VibePlugin) addToBuffer(ctx MessageContext) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
buf := p.buffers[ctx.RoomID]
|
|
buf = append(buf, bufferedMessage{
|
|
Sender: string(ctx.Sender),
|
|
Body: ctx.Body,
|
|
Time: time.Now().UTC(),
|
|
})
|
|
|
|
// Keep only the last N messages
|
|
if len(buf) > vibeBufferSize {
|
|
buf = buf[len(buf)-vibeBufferSize:]
|
|
}
|
|
|
|
p.buffers[ctx.RoomID] = buf
|
|
}
|
|
|
|
func (p *VibePlugin) getBuffer(roomID id.RoomID) []bufferedMessage {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
buf := p.buffers[roomID]
|
|
// Return a copy
|
|
result := make([]bufferedMessage, len(buf))
|
|
copy(result, buf)
|
|
return result
|
|
}
|
|
|
|
func (p *VibePlugin) checkCooldown(roomID id.RoomID) bool {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
last, ok := p.cooldowns[roomID]
|
|
if ok && time.Since(last) < vibeCooldownMins*time.Minute {
|
|
return false
|
|
}
|
|
p.cooldowns[roomID] = time.Now()
|
|
return true
|
|
}
|
|
|
|
func (p *VibePlugin) resetCooldown(roomID id.RoomID) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
delete(p.cooldowns, roomID)
|
|
}
|
|
|
|
func (p *VibePlugin) handleVibe(ctx MessageContext) error {
|
|
ollamaHost := os.Getenv("OLLAMA_HOST")
|
|
ollamaModel := os.Getenv("OLLAMA_MODEL")
|
|
if ollamaHost == "" || ollamaModel == "" {
|
|
return p.SendReply(ctx.RoomID, ctx.EventID, "LLM is not configured.")
|
|
}
|
|
|
|
buf := p.getBuffer(ctx.RoomID)
|
|
if len(buf) < vibeMinMessages {
|
|
return p.SendReply(ctx.RoomID, ctx.EventID,
|
|
fmt.Sprintf("Need at least %d messages to read the vibe. Currently have %d.", vibeMinMessages, len(buf)))
|
|
}
|
|
|
|
if !p.checkCooldown(ctx.RoomID) {
|
|
return p.SendReply(ctx.RoomID, ctx.EventID,
|
|
fmt.Sprintf("Vibe check is on cooldown. Try again in %d minutes.", vibeCooldownMins))
|
|
}
|
|
|
|
botName := os.Getenv("BOT_DISPLAY_NAME")
|
|
if botName == "" {
|
|
botName = "GogoBee"
|
|
}
|
|
transcript := formatTranscript(buf)
|
|
prompt := fmt.Sprintf(
|
|
`You are %s, a fun community bot. Based on the following recent chat messages, describe the current "vibe" or energy of the room in 2-3 sentences. Be creative, playful, and use colorful language. Reference specific topics or dynamics you notice.
|
|
|
|
Recent messages:
|
|
%s
|
|
|
|
Describe the room's current vibe:`, botName, transcript)
|
|
|
|
if err := p.SendReply(ctx.RoomID, ctx.EventID, "Reading the room..."); err != nil {
|
|
slog.Error("vibe: send thinking", "err", err)
|
|
}
|
|
|
|
response, err := callOllama(ollamaHost, ollamaModel, prompt)
|
|
if err != nil {
|
|
slog.Error("vibe: ollama call", "err", err)
|
|
p.resetCooldown(ctx.RoomID) // Don't consume cooldown on failure
|
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to read the vibe. LLM might be offline.")
|
|
}
|
|
|
|
return p.SendMessage(ctx.RoomID, response)
|
|
}
|
|
|
|
func (p *VibePlugin) handleTLDR(ctx MessageContext) error {
|
|
ollamaHost := os.Getenv("OLLAMA_HOST")
|
|
ollamaModel := os.Getenv("OLLAMA_MODEL")
|
|
if ollamaHost == "" || ollamaModel == "" {
|
|
return p.SendReply(ctx.RoomID, ctx.EventID, "LLM is not configured.")
|
|
}
|
|
|
|
buf := p.getBuffer(ctx.RoomID)
|
|
if len(buf) < vibeMinMessages {
|
|
return p.SendReply(ctx.RoomID, ctx.EventID,
|
|
fmt.Sprintf("Need at least %d messages for a summary. Currently have %d.", vibeMinMessages, len(buf)))
|
|
}
|
|
|
|
if !p.checkCooldown(ctx.RoomID) {
|
|
return p.SendReply(ctx.RoomID, ctx.EventID,
|
|
fmt.Sprintf("TLDR is on cooldown. Try again in %d minutes.", vibeCooldownMins))
|
|
}
|
|
|
|
tldrBotName := os.Getenv("BOT_DISPLAY_NAME")
|
|
if tldrBotName == "" {
|
|
tldrBotName = "GogoBee"
|
|
}
|
|
transcript := formatTranscript(buf)
|
|
prompt := fmt.Sprintf(
|
|
`You are %s, a community bot. Summarize the following recent chat conversation in 3-5 concise bullet points. Focus on the main topics discussed, any decisions made, and key moments. Be brief and informative.
|
|
|
|
Recent messages:
|
|
%s
|
|
|
|
Summary:`, tldrBotName, transcript)
|
|
|
|
if err := p.SendReply(ctx.RoomID, ctx.EventID, "Summarizing the conversation..."); err != nil {
|
|
slog.Error("vibe: send thinking", "err", err)
|
|
}
|
|
|
|
response, err := callOllama(ollamaHost, ollamaModel, prompt)
|
|
if err != nil {
|
|
slog.Error("vibe: ollama call", "err", err)
|
|
p.resetCooldown(ctx.RoomID) // Don't consume cooldown on failure
|
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to summarize. LLM might be offline.")
|
|
}
|
|
|
|
return p.SendMessage(ctx.RoomID, response)
|
|
}
|
|
|
|
func formatTranscript(messages []bufferedMessage) string {
|
|
var sb strings.Builder
|
|
for _, m := range messages {
|
|
// Truncate long messages
|
|
body := m.Body
|
|
if len(body) > 300 {
|
|
body = body[:300] + "..."
|
|
}
|
|
sb.WriteString(fmt.Sprintf("[%s] %s: %s\n", m.Time.Format("15:04"), m.Sender, body))
|
|
}
|
|
return sb.String()
|
|
}
|