224 lines
6.0 KiB
Go
224 lines
6.0 KiB
Go
// 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",
|
||
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
|
||
for _, cmd := range commands {
|
||
_, err := dg.ApplicationCommandCreate(dg.State.User.ID, guildID, cmd)
|
||
if err != nil {
|
||
log.Printf("⚠️ Slash-Command /%s konnte nicht registriert werden: %v", cmd.Name, err)
|
||
} else {
|
||
scope := "global"
|
||
if guildID != "" {
|
||
scope = "guild " + guildID
|
||
}
|
||
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())
|
||
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) {
|
||
// Sofort mit "Denke nach..." antworten (Discord-Timeout: 3s)
|
||
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
|
||
})
|
||
|
||
reply := queryAndFormat(question)
|
||
|
||
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()
|
||
author := i.Member.User.Username
|
||
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()
|
||
}
|