mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-19 11:41:23 +02:00
Replace 21 separate NestJS Matrix bot processes (~2.1 GB RAM, ~4.2 GB Docker images) with a single Go binary using plugin architecture (8.6 MB binary, ~30 MB RAM). New services: - services/mana-matrix-bot/ — Go Matrix bot with 21 plugins (mautrix-go, Redis sessions) - services/mana-api-gateway-go/ — Go API gateway (rate limiting, API keys, credit billing) Deleted: - 21 services/matrix-*-bot/ directories - packages/bot-services/ and packages/matrix-bot-common/ - Legacy deploy scripts and CI build jobs Updated: - docker-compose.macmini.yml: new Go services, legacy bots removed - CI/CD: change detection + build jobs for Go services - Root package.json: new dev:matrix, build:matrix, test:matrix scripts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
392 lines
12 KiB
Go
392 lines
12 KiB
Go
package zitare
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"github.com/manacore/mana-matrix-bot/internal/plugin"
|
|
"github.com/manacore/mana-matrix-bot/internal/services"
|
|
)
|
|
|
|
func init() {
|
|
plugin.Register("zitare", func() plugin.Plugin { return &ZitarePlugin{} })
|
|
}
|
|
|
|
// Quote represents a quote from the backend.
|
|
type Quote struct {
|
|
ID string `json:"id"`
|
|
Text string `json:"text"`
|
|
Author string `json:"author"`
|
|
Category string `json:"category"`
|
|
}
|
|
|
|
// Category represents a quote category.
|
|
type Category struct {
|
|
Name string `json:"name"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
// ZitarePlugin implements the Matrix quotes bot.
|
|
type ZitarePlugin struct {
|
|
backend *services.BackendClient
|
|
router *plugin.CommandRouter
|
|
detector *plugin.KeywordDetector
|
|
lastQuote map[string]*Quote // per-user last shown quote
|
|
}
|
|
|
|
func (p *ZitarePlugin) Name() string { return "zitare" }
|
|
|
|
func (p *ZitarePlugin) Init(_ context.Context, cfg plugin.PluginConfig) error {
|
|
if cfg.BackendURL == "" {
|
|
return fmt.Errorf("zitare plugin requires BackendURL")
|
|
}
|
|
p.backend = services.NewBackendClient(cfg.BackendURL)
|
|
p.lastQuote = make(map[string]*Quote)
|
|
|
|
p.router = plugin.NewCommandRouter()
|
|
p.router.Handle("!help", p.cmdHelp)
|
|
p.router.Handle("!hilfe", p.cmdHelp)
|
|
p.router.Handle("!zitat", p.cmdRandom)
|
|
p.router.Handle("!quote", p.cmdRandom)
|
|
p.router.Handle("!heute", p.cmdToday)
|
|
p.router.Handle("!today", p.cmdToday)
|
|
p.router.Handle("!suche", p.cmdSearch)
|
|
p.router.Handle("!search", p.cmdSearch)
|
|
p.router.Handle("!kategorie", p.cmdCategory)
|
|
p.router.Handle("!category", p.cmdCategory)
|
|
p.router.Handle("!kategorien", p.cmdCategories)
|
|
p.router.Handle("!categories", p.cmdCategories)
|
|
p.router.Handle("!motivation", p.cmdMotivation)
|
|
p.router.Handle("!morgen", p.cmdMorning)
|
|
p.router.Handle("!favorit", p.cmdFavorite)
|
|
p.router.Handle("!fav", p.cmdFavorite)
|
|
p.router.Handle("!favoriten", p.cmdFavorites)
|
|
p.router.Handle("!favorites", p.cmdFavorites)
|
|
p.router.Handle("!listen", p.cmdLists)
|
|
p.router.Handle("!lists", p.cmdLists)
|
|
p.router.Handle("!status", p.cmdStatus)
|
|
|
|
p.detector = plugin.NewKeywordDetector(append(plugin.CommonKeywords,
|
|
plugin.KeywordCommand{Keywords: []string{"zitat", "quote", "inspiration", "inspiriere"}, Command: "random"},
|
|
plugin.KeywordCommand{Keywords: []string{"tageszitat"}, Command: "today"},
|
|
plugin.KeywordCommand{Keywords: []string{"motiviere", "motivation", "motivier mich"}, Command: "motivation"},
|
|
plugin.KeywordCommand{Keywords: []string{"guten morgen", "good morning"}, Command: "morning"},
|
|
plugin.KeywordCommand{Keywords: []string{"kategorien", "categories", "themen"}, Command: "categories"},
|
|
plugin.KeywordCommand{Keywords: []string{"favoriten", "favorites", "meine favoriten"}, Command: "favorites"},
|
|
plugin.KeywordCommand{Keywords: []string{"listen", "lists", "meine listen"}, Command: "lists"},
|
|
))
|
|
|
|
slog.Info("zitare plugin initialized", "backend", cfg.BackendURL)
|
|
return nil
|
|
}
|
|
|
|
func (p *ZitarePlugin) Commands() []plugin.CommandDef {
|
|
return []plugin.CommandDef{
|
|
{Patterns: []string{"!zitat", "!quote"}, Description: "Zufälliges Zitat", Category: "Zitate"},
|
|
{Patterns: []string{"!heute", "!today"}, Description: "Zitat des Tages", Category: "Zitate"},
|
|
{Patterns: []string{"!suche [text]"}, Description: "Zitate suchen", Category: "Zitate"},
|
|
{Patterns: []string{"!kategorie [name]"}, Description: "Zitat aus Kategorie", Category: "Zitate"},
|
|
{Patterns: []string{"!kategorien"}, Description: "Alle Kategorien", Category: "Zitate"},
|
|
{Patterns: []string{"!motivation"}, Description: "Motivationszitat", Category: "Zitate"},
|
|
{Patterns: []string{"!favorit"}, Description: "Letztes Zitat als Favorit", Category: "Zitate"},
|
|
{Patterns: []string{"!favoriten"}, Description: "Alle Favoriten", Category: "Zitate"},
|
|
}
|
|
}
|
|
|
|
func (p *ZitarePlugin) HandleTextMessage(ctx context.Context, mc *plugin.MessageContext) error {
|
|
matched, err := p.router.Route(mc)
|
|
if matched {
|
|
return err
|
|
}
|
|
|
|
cmd := p.detector.Detect(mc.Body)
|
|
switch cmd {
|
|
case "help":
|
|
return p.cmdHelp(mc, "")
|
|
case "random":
|
|
return p.cmdRandom(mc, "")
|
|
case "today":
|
|
return p.cmdToday(mc, "")
|
|
case "motivation":
|
|
return p.cmdMotivation(mc, "")
|
|
case "morning":
|
|
return p.cmdMorning(mc, "")
|
|
case "categories":
|
|
return p.cmdCategories(mc, "")
|
|
case "favorites":
|
|
return p.cmdFavorites(mc, "")
|
|
case "lists":
|
|
return p.cmdLists(mc, "")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// --- Command Handlers ---
|
|
|
|
func (p *ZitarePlugin) cmdRandom(mc *plugin.MessageContext, _ string) error {
|
|
ctx := context.Background()
|
|
token, _ := mc.Session.Manager.GetToken(mc.Session.UserID)
|
|
|
|
var quote Quote
|
|
if err := p.backend.Get(ctx, "/api/v1/quotes/random", token, "e); err != nil {
|
|
slog.Error("get random quote failed", "error", err)
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, "❌ Zitat konnte nicht geladen werden.")
|
|
return nil
|
|
}
|
|
|
|
p.lastQuote[mc.Sender] = "e
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, formatQuote("e))
|
|
return nil
|
|
}
|
|
|
|
func (p *ZitarePlugin) cmdToday(mc *plugin.MessageContext, _ string) error {
|
|
ctx := context.Background()
|
|
token, _ := mc.Session.Manager.GetToken(mc.Session.UserID)
|
|
|
|
var quote Quote
|
|
if err := p.backend.Get(ctx, "/api/v1/quotes/today", token, "e); err != nil {
|
|
slog.Error("get today quote failed", "error", err)
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, "❌ Zitat des Tages konnte nicht geladen werden.")
|
|
return nil
|
|
}
|
|
|
|
p.lastQuote[mc.Sender] = "e
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, formatQuote("e))
|
|
return nil
|
|
}
|
|
|
|
func (p *ZitarePlugin) cmdSearch(mc *plugin.MessageContext, args string) error {
|
|
ctx := context.Background()
|
|
if args == "" {
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, "Bitte gib einen Suchbegriff an.\n\nBeispiel: `!suche Glück`")
|
|
return nil
|
|
}
|
|
|
|
token, _ := mc.Session.Manager.GetToken(mc.Session.UserID)
|
|
|
|
var quotes []Quote
|
|
if err := p.backend.Get(ctx, "/api/v1/quotes/search?q="+args, token, "es); err != nil {
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, "❌ Suche fehlgeschlagen.")
|
|
return nil
|
|
}
|
|
|
|
if len(quotes) == 0 {
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, fmt.Sprintf("📭 Keine Zitate für \"%s\" gefunden.", args))
|
|
return nil
|
|
}
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString(fmt.Sprintf("**Suchergebnisse für \"%s\" (%d):**\n\n", args, len(quotes)))
|
|
|
|
limit := len(quotes)
|
|
if limit > 5 {
|
|
limit = 5
|
|
}
|
|
for i := 0; i < limit; i++ {
|
|
q := quotes[i]
|
|
sb.WriteString(fmt.Sprintf("**%d.** \"%s\"\n-- *%s*\n\n", i+1, q.Text, q.Author))
|
|
}
|
|
if len(quotes) > 5 {
|
|
sb.WriteString(fmt.Sprintf("_...und %d weitere_", len(quotes)-5))
|
|
}
|
|
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, sb.String())
|
|
return nil
|
|
}
|
|
|
|
func (p *ZitarePlugin) cmdCategory(mc *plugin.MessageContext, args string) error {
|
|
ctx := context.Background()
|
|
if args == "" {
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, "Bitte gib eine Kategorie an.\n\nBeispiel: `!kategorie motivation`\nAlle Kategorien: `!kategorien`")
|
|
return nil
|
|
}
|
|
|
|
token, _ := mc.Session.Manager.GetToken(mc.Session.UserID)
|
|
|
|
var quote Quote
|
|
if err := p.backend.Get(ctx, "/api/v1/quotes/random?category="+args, token, "e); err != nil {
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, fmt.Sprintf("❌ Kein Zitat in Kategorie \"%s\" gefunden.", args))
|
|
return nil
|
|
}
|
|
|
|
p.lastQuote[mc.Sender] = "e
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, formatQuote("e))
|
|
return nil
|
|
}
|
|
|
|
func (p *ZitarePlugin) cmdCategories(mc *plugin.MessageContext, _ string) error {
|
|
ctx := context.Background()
|
|
token, _ := mc.Session.Manager.GetToken(mc.Session.UserID)
|
|
|
|
var categories []Category
|
|
if err := p.backend.Get(ctx, "/api/v1/quotes/categories", token, &categories); err != nil {
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, "❌ Kategorien konnten nicht geladen werden.")
|
|
return nil
|
|
}
|
|
|
|
if len(categories) == 0 {
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, "📭 Keine Kategorien verfügbar.")
|
|
return nil
|
|
}
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString("**Verfügbare Kategorien:**\n\n")
|
|
for _, cat := range categories {
|
|
sb.WriteString(fmt.Sprintf("• **%s** (`!kategorie %s`) - %d Zitate\n", cat.Name, strings.ToLower(cat.Name), cat.Count))
|
|
}
|
|
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, sb.String())
|
|
return nil
|
|
}
|
|
|
|
func (p *ZitarePlugin) cmdMotivation(mc *plugin.MessageContext, _ string) error {
|
|
return p.cmdCategory(mc, "motivation")
|
|
}
|
|
|
|
func (p *ZitarePlugin) cmdMorning(mc *plugin.MessageContext, _ string) error {
|
|
return p.cmdCategory(mc, "motivation")
|
|
}
|
|
|
|
func (p *ZitarePlugin) cmdFavorite(mc *plugin.MessageContext, _ string) error {
|
|
ctx := context.Background()
|
|
|
|
token, ok := mc.Session.Manager.GetToken(mc.Session.UserID)
|
|
if !ok {
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, "Bitte melde dich zuerst an: `!login email passwort`")
|
|
return nil
|
|
}
|
|
|
|
quote, ok := p.lastQuote[mc.Sender]
|
|
if !ok {
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, "❌ Kein Zitat zum Speichern. Hole zuerst eines: `!zitat`")
|
|
return nil
|
|
}
|
|
|
|
body := map[string]string{"quoteId": quote.ID}
|
|
if err := p.backend.Post(ctx, "/api/v1/favorites", token, body, nil); err != nil {
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, "❌ Favorit konnte nicht gespeichert werden.")
|
|
return nil
|
|
}
|
|
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, "⭐ Zitat als Favorit gespeichert!")
|
|
return nil
|
|
}
|
|
|
|
func (p *ZitarePlugin) cmdFavorites(mc *plugin.MessageContext, _ string) error {
|
|
ctx := context.Background()
|
|
|
|
token, ok := mc.Session.Manager.GetToken(mc.Session.UserID)
|
|
if !ok {
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, "Bitte melde dich zuerst an: `!login email passwort`")
|
|
return nil
|
|
}
|
|
|
|
var favorites []Quote
|
|
if err := p.backend.Get(ctx, "/api/v1/favorites", token, &favorites); err != nil {
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, "❌ Favoriten konnten nicht geladen werden.")
|
|
return nil
|
|
}
|
|
|
|
if len(favorites) == 0 {
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, "📭 Keine Favoriten.\n\nSpeichere ein Zitat mit `!favorit` nach `!zitat`.")
|
|
return nil
|
|
}
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString(fmt.Sprintf("**⭐ Deine Favoriten (%d):**\n\n", len(favorites)))
|
|
|
|
limit := len(favorites)
|
|
if limit > 10 {
|
|
limit = 10
|
|
}
|
|
for i := 0; i < limit; i++ {
|
|
q := favorites[i]
|
|
sb.WriteString(fmt.Sprintf("**%d.** \"%s\"\n-- *%s*\n\n", i+1, q.Text, q.Author))
|
|
}
|
|
if len(favorites) > 10 {
|
|
sb.WriteString(fmt.Sprintf("_...und %d weitere_", len(favorites)-10))
|
|
}
|
|
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, sb.String())
|
|
return nil
|
|
}
|
|
|
|
func (p *ZitarePlugin) cmdLists(mc *plugin.MessageContext, _ string) error {
|
|
ctx := context.Background()
|
|
|
|
token, ok := mc.Session.Manager.GetToken(mc.Session.UserID)
|
|
if !ok {
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, "Bitte melde dich zuerst an: `!login email passwort`")
|
|
return nil
|
|
}
|
|
|
|
var lists []struct {
|
|
Name string `json:"name"`
|
|
Count int `json:"count"`
|
|
}
|
|
if err := p.backend.Get(ctx, "/api/v1/lists", token, &lists); err != nil {
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, "❌ Listen konnten nicht geladen werden.")
|
|
return nil
|
|
}
|
|
|
|
if len(lists) == 0 {
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, "📭 Keine Listen.\n\nErstelle eine mit: `!liste Meine Zitate`")
|
|
return nil
|
|
}
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString("**📋 Deine Listen:**\n\n")
|
|
for i, l := range lists {
|
|
sb.WriteString(fmt.Sprintf("**%d.** %s (%d Zitate)\n", i+1, l.Name, l.Count))
|
|
}
|
|
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, sb.String())
|
|
return nil
|
|
}
|
|
|
|
func (p *ZitarePlugin) cmdStatus(mc *plugin.MessageContext, _ string) error {
|
|
ctx := context.Background()
|
|
loggedIn := mc.Session.Manager.IsLoggedIn(mc.Session.UserID)
|
|
status := "❌ Nicht angemeldet"
|
|
if loggedIn {
|
|
status = "✅ Angemeldet"
|
|
}
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, fmt.Sprintf("**Zitare Bot Status**\n\n%s", status))
|
|
return nil
|
|
}
|
|
|
|
func (p *ZitarePlugin) cmdHelp(mc *plugin.MessageContext, _ string) error {
|
|
ctx := context.Background()
|
|
help := `**📜 Zitare Bot - Befehle**
|
|
|
|
**Zitate:**
|
|
• ` + "`!zitat`" + ` — Zufälliges Zitat
|
|
• ` + "`!heute`" + ` — Zitat des Tages
|
|
• ` + "`!suche Glück`" + ` — Zitate suchen
|
|
• ` + "`!kategorie motivation`" + ` — Zitat aus Kategorie
|
|
• ` + "`!kategorien`" + ` — Alle Kategorien
|
|
• ` + "`!motivation`" + ` — Motivationszitat
|
|
|
|
**Favoriten:**
|
|
• ` + "`!favorit`" + ` — Letztes Zitat als Favorit speichern
|
|
• ` + "`!favoriten`" + ` — Alle Favoriten
|
|
|
|
**Listen:**
|
|
• ` + "`!listen`" + ` — Alle Listen`
|
|
|
|
mc.Client.SendReply(ctx, mc.RoomID, mc.EventID, help)
|
|
return nil
|
|
}
|
|
|
|
// --- Formatting ---
|
|
|
|
func formatQuote(q *Quote) string {
|
|
author := q.Author
|
|
if author == "" {
|
|
author = "Unbekannt"
|
|
}
|
|
return fmt.Sprintf("\"%s\"\n-- *%s*", q.Text, author)
|
|
}
|