// discord – Discord-Bot für my-brain-importer // Unterstützt /ask, /ingest und @Mention package main import ( "fmt" "log" "os" "os/signal" "strings" "syscall" "github.com/bwmarrin/discordgo" "my-brain-importer/internal/brain" "my-brain-importer/internal/config" ) var ( dg *discordgo.Session botUser *discordgo.User commands = []*discordgo.ApplicationCommand{ { Name: "ask", Description: "Stelle eine Frage an deinen AI Brain (mit Wissensdatenbank)", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionString, Name: "frage", Description: "Die Frage, die du stellen möchtest", Required: true, }, }, }, { Name: "asknobrain", Description: "Stelle eine Frage direkt ans LLM (ohne Wissensdatenbank)", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionString, Name: "frage", Description: "Die Frage, die du stellen möchtest", Required: true, }, }, }, { Name: "ingest", Description: "Importiert Markdown-Notizen aus brain_root in die Wissensdatenbank", }, { Name: "remember", Description: "Speichert eine Nachricht in der Wissensdatenbank", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionString, Name: "text", Description: "Der Text, der gespeichert werden soll", Required: true, }, }, }, } ) func main() { config.LoadConfig() token := config.Cfg.Discord.Token if token == "" || token == "dein-discord-bot-token" { log.Fatal("❌ Kein Discord-Token in config.yml konfiguriert (discord.token)") } var err error dg, err = discordgo.New("Bot " + token) if err != nil { log.Fatalf("❌ Discord-Session konnte nicht erstellt werden: %v", err) } dg.AddHandler(onReady) dg.AddHandler(onInteraction) dg.AddHandler(onMessage) dg.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildMessages | discordgo.IntentMessageContent if err = dg.Open(); err != nil { log.Fatalf("❌ Verbindung zu Discord fehlgeschlagen: %v", err) } defer dg.Close() registerCommands() fmt.Println("✅ Bot läuft. Drücke CTRL+C zum Beenden.") stop := make(chan os.Signal, 1) signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) <-stop fmt.Println("\n👋 Bot wird beendet...") } func onReady(s *discordgo.Session, r *discordgo.Ready) { botUser = r.User fmt.Printf("✅ Eingeloggt als %s#%s\n", r.User.Username, r.User.Discriminator) } func registerCommands() { guildID := config.Cfg.Discord.GuildID registered, err := dg.ApplicationCommandBulkOverwrite(dg.State.User.ID, guildID, commands) if err != nil { log.Fatalf("❌ Slash-Commands konnten nicht registriert werden: %v", err) } scope := "global" if guildID != "" { scope = "guild " + guildID } for _, cmd := range registered { fmt.Printf("📝 Slash-Command /%s registriert (%s)\n", cmd.Name, scope) } } func onInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) { if i.Type != discordgo.InteractionApplicationCommand { return } switch i.ApplicationCommandData().Name { case "ask": handleAsk(s, i, i.ApplicationCommandData().Options[0].StringValue(), true) case "asknobrain": handleAsk(s, i, i.ApplicationCommandData().Options[0].StringValue(), false) case "ingest": handleIngest(s, i) case "remember": handleRemember(s, i) } } func onMessage(s *discordgo.Session, m *discordgo.MessageCreate) { if m.Author.Bot { return } if botUser == nil { return } mentioned := false for _, u := range m.Mentions { if u.ID == botUser.ID { mentioned = true break } } if !mentioned { return } // Mention aus der Nachricht entfernen question := strings.TrimSpace( strings.ReplaceAll(m.Content, "<@"+botUser.ID+">", ""), ) if question == "" { s.ChannelMessageSend(m.ChannelID, "Stell mir eine Frage! Beispiel: @Brain Was sind meine TODOs?") return } s.ChannelTyping(m.ChannelID) reply := queryAndFormat(question) s.ChannelMessageSendReply(m.ChannelID, reply, m.Reference()) } func handleAsk(s *discordgo.Session, i *discordgo.InteractionCreate, question string, useBrain bool) { s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, }) var reply string if useBrain { reply = queryAndFormat(question) } else { answer, err := brain.ChatDirect(question) if err != nil { reply = fmt.Sprintf("❌ Fehler: %v", err) } else { reply = fmt.Sprintf("💬 **Antwort:** _%s_\n\n%s", question, answer) } } s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ Content: &reply, }) } func handleIngest(s *discordgo.Session, i *discordgo.InteractionCreate) { s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, }) fmt.Println("📥 Ingest gestartet via Discord...") brain.RunIngest(config.Cfg.BrainRoot) msg := fmt.Sprintf("✅ Ingest abgeschlossen! Quelle: `%s`", config.Cfg.BrainRoot) s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ Content: &msg, }) } func handleRemember(s *discordgo.Session, i *discordgo.InteractionCreate) { s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, }) text := i.ApplicationCommandData().Options[0].StringValue() var author string if i.Member != nil { author = i.Member.User.Username } else if i.User != nil { author = i.User.Username } else { author = "unknown" } source := fmt.Sprintf("discord/#%s", i.ChannelID) err := brain.IngestChatMessage(text, author, source) var msg string if err != nil { msg = fmt.Sprintf("❌ Fehler beim Speichern: %v", err) } else { msg = fmt.Sprintf("✅ Gespeichert: _%s_", text) } s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{Content: &msg}) } func queryAndFormat(question string) string { answer, chunks, err := brain.AskQuery(question) if err != nil { return fmt.Sprintf("❌ Fehler: %v", err) } if len(chunks) == 0 { return "❌ Keine relevanten Informationen in der Datenbank gefunden.\nFüge mehr Daten mit `/ingest` hinzu." } var sb strings.Builder fmt.Fprintf(&sb, "💬 **Antwort auf:** _%s_\n\n", question) sb.WriteString(answer) sb.WriteString("\n\n📚 **Quellen:**\n") for _, chunk := range chunks { fmt.Fprintf(&sb, "• %.1f%% – %s\n", chunk.Score*100, chunk.Source) } return sb.String() }