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:
221
internal/plugin/vibe.go
Normal file
221
internal/plugin/vibe.go
Normal file
@@ -0,0 +1,221 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user