This commit is contained in:
2025-08-11 08:30:05 +02:00
parent c1505d52cd
commit e3bf24c66e
7 changed files with 399 additions and 110 deletions

420
main.go
View File

@@ -1,6 +1,7 @@
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
@@ -11,63 +12,24 @@ import (
"time"
"github.com/bwmarrin/discordgo"
_ "modernc.org/sqlite" // SQLite3-Treiber importieren
)
// ===== Defaults / Names (pro Guild per Commands konfigurierbar) =====
const (
var (
pollInterval = 15 * time.Second
defaultLobbyName = " Erstelle privaten Raum"
defaultCategory = "Private Räume"
)
// Pfad zur Persistenz-Datei (überschreibbar via CONFIG_PATH)
var configPath = func() string {
if v := os.Getenv("CONFIG_PATH"); v != "" {
// Pfad zur SQLite-Datenbank
var dbPath = func() string {
if v := os.Getenv("DB_PATH"); v != "" {
return v
}
return "guild_config.json"
return "guild_config.db"
}()
// Persistenz der Guild-Konfiguration
func loadGuildCfgs() error {
f, err := os.Open(configPath)
if err != nil {
return err
}
defer f.Close()
var m map[string]*GuildConfig
if err := json.NewDecoder(f).Decode(&m); err != nil {
return err
}
cfgMu.Lock()
for k, v := range m {
guildCfgs[k] = v
}
cfgMu.Unlock()
return nil
}
func saveGuildCfgs() error {
tmp := configPath + ".tmp"
f, err := os.Create(tmp)
if err != nil {
return err
}
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
cfgMu.RLock()
err = enc.Encode(guildCfgs)
cfgMu.RUnlock()
_ = f.Close()
if err != nil {
return err
}
return os.Rename(tmp, configPath)
}
// ===== Per-Guild Config (in-memory) =====
type GuildConfig struct {
LobbyName string `json:"lobby_name"`
@@ -75,29 +37,116 @@ type GuildConfig struct {
TimeoutMin int `json:"timeout_min"`
}
var (
cfgMu sync.RWMutex
guildCfgs = map[string]*GuildConfig{}
createdCmds = map[string][]*discordgo.ApplicationCommand{} // guildID -> cmds
registerOnce sync.Once
)
var db *sql.DB
// ===== Global variables for sync.Map =====
var guildCfgs sync.Map // Für Guild-Konfigurationen
var createdCmds sync.Map // Für erstellte Commands
// Initialize DB
func initDB() {
var err error
db, err = sql.Open("sqlite", dbPath)
if err != nil {
log.Fatalf("Datenbank-Fehler: %v", err)
}
// Tabelle erstellen, falls sie noch nicht existiert
createTableSQL := `CREATE TABLE IF NOT EXISTS guild_config (
guild_id TEXT PRIMARY KEY,
lobby_name TEXT,
category_name TEXT,
timeout_min INTEGER
);`
_, err = db.Exec(createTableSQL)
if err != nil {
log.Fatalf("Fehler beim Erstellen der Tabelle: %v", err)
}
}
// Close DB connection
func closeDB() {
if err := db.Close(); err != nil {
log.Fatalf("Fehler beim Schließen der DB: %v", err)
}
}
// Laden der Guild-Konfiguration aus der Datenbank
func loadGuildCfgs() error {
rows, err := db.Query("SELECT guild_id, lobby_name, category_name, timeout_min FROM guild_config")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var guildID, lobbyName, categoryName string
var timeoutMin int
if err := rows.Scan(&guildID, &lobbyName, &categoryName, &timeoutMin); err != nil {
return err
}
guildCfgs.Store(guildID, &GuildConfig{
LobbyName: lobbyName,
CategoryName: categoryName,
TimeoutMin: timeoutMin,
})
}
return nil
}
// Speichern der Guild-Konfiguration in die Datenbank
func saveGuildCfgs() error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
guildCfgs.Range(func(key, value interface{}) bool {
guildID := key.(string)
cfg := value.(*GuildConfig)
_, err := tx.Exec(`
INSERT INTO guild_config (guild_id, lobby_name, category_name, timeout_min)
VALUES (?, ?, ?, ?)
ON CONFLICT(guild_id)
DO UPDATE SET
lobby_name = excluded.lobby_name,
category_name = excluded.category_name,
timeout_min = excluded.timeout_min
`, guildID, cfg.LobbyName, cfg.CategoryName, cfg.TimeoutMin)
if err != nil {
log.Printf("Fehler beim Speichern der Guild-Konfiguration: %v", err)
}
return true
})
// Transaktion abschließen
return tx.Commit()
}
// Guild-Konfiguration laden oder Standardwerte zurückgeben
func getCfg(guildID string) *GuildConfig {
cfgMu.RLock()
c, ok := guildCfgs[guildID]
cfgMu.RUnlock()
if ok {
return c
if cfg, ok := guildCfgs.Load(guildID); ok {
return cfg.(*GuildConfig)
}
cfgMu.Lock()
defer cfgMu.Unlock()
c = &GuildConfig{
LobbyName: defaultLobbyName,
CategoryName: defaultCategory,
TimeoutMin: envTimeoutDefault(60),
var guildCfg GuildConfig
err := db.QueryRow("SELECT lobby_name, category_name, timeout_min FROM guild_config WHERE guild_id = ?", guildID).Scan(&guildCfg.LobbyName, &guildCfg.CategoryName, &guildCfg.TimeoutMin)
if err != nil {
if err == sql.ErrNoRows {
log.Printf("Guild-Konfiguration für %s nicht gefunden, verwenden der Standardwerte", guildID)
} else {
log.Printf("Fehler beim Abrufen der Guild-Konfiguration für %s: %v", guildID, err)
}
guildCfg = GuildConfig{
LobbyName: defaultLobbyName,
CategoryName: defaultCategory,
TimeoutMin: envTimeoutDefault(1),
}
}
guildCfgs[guildID] = c
return c
guildCfgs.Store(guildID, &guildCfg)
return &guildCfg
}
func envTimeoutDefault(def int) int {
@@ -262,7 +311,7 @@ func createPrivateVCAndMove(
break
}
wait := time.Duration(150*(attempt+1)) * time.Millisecond
log.Printf("Move fehlgeschlagen (Versuch %d): %v retry in %s", attempt+1, moveErr, wait)
log.Printf("Move fehlgeschlagen (Versuch %d): %v - retry in %s", attempt+1, moveErr, wait)
time.Sleep(wait)
}
if moveErr != nil {
@@ -427,57 +476,129 @@ func onInteractionCreate(_ string) func(s *discordgo.Session, i *discordgo.Inter
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: msg, Flags: discordgo.MessageFlagsEphemeral}})
case "setlobby":
// Admin-Check zuerst
if !isGuildAdmin(s, guildID, i.User, i.Member) {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "❌ Nur Administratoren dürfen das.", Flags: discordgo.MessageFlagsEphemeral}})
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "❌ Nur Administratoren dürfen das.",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
// Option "name" lesen
var name string
for _, o := range data.Options {
if o.Name == "name" {
name = o.StringValue()
break
}
}
if name == "" {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "Bitte gib einen Namen an.", Flags: discordgo.MessageFlagsEphemeral}})
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Bitte gib einen Namen an.",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
cfgMu.Lock()
// speichern
getCfg(guildID).LobbyName = name
cfgMu.Unlock()
if err := saveGuildCfgs(); err != nil {
log.Printf("Speichern fehlgeschlagen: %v", err)
log.Printf("saveGuildCfgs failed: %v", err)
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "⚠️ Lobby-Name gesetzt, aber Speichern fehlgeschlagen. Schau die Logs.",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "✅ Lobby-Name aktualisiert auf: " + name, Flags: discordgo.MessageFlagsEphemeral}})
// Antwort <= 3s
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "✅ Lobby-Name aktualisiert auf: " + name,
Flags: discordgo.MessageFlagsEphemeral,
},
})
case "setcategory":
// Admin-Check zuerst
if !isGuildAdmin(s, guildID, i.User, i.Member) {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "❌ Nur Administratoren dürfen das.", Flags: discordgo.MessageFlagsEphemeral}})
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "❌ Nur Administratoren dürfen das.",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
// Option "name" lesen
var name string
for _, o := range data.Options {
if o.Name == "name" {
name = o.StringValue()
break
}
}
if name == "" {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "Bitte gib einen Namen an.", Flags: discordgo.MessageFlagsEphemeral}})
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Bitte gib einen Namen an.",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
cfgMu.Lock()
// speichern
getCfg(guildID).CategoryName = name
cfgMu.Unlock()
if err := saveGuildCfgs(); err != nil {
log.Printf("Speichern fehlgeschlagen: %v", err)
log.Printf("saveGuildCfgs failed: %v", err)
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "⚠️ Kategorie-Name gesetzt, aber Speichern fehlgeschlagen. Schau die Logs.",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "✅ Kategorie-Name aktualisiert auf: " + name, Flags: discordgo.MessageFlagsEphemeral}})
// Antwort <= 3s
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "✅ Kategorie-Name aktualisiert auf: " + name,
Flags: discordgo.MessageFlagsEphemeral,
},
})
case "settimeout":
if !isGuildAdmin(s, guildID, i.User, i.Member) {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "❌ Nur Administratoren dürfen das.", Flags: discordgo.MessageFlagsEphemeral}})
log.Println("User is not an admin")
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "❌ Nur Administratoren dürfen das.",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
var minutes int64 = int64(getCfg(guildID).TimeoutMin)
minutes := int64(getCfg(guildID).TimeoutMin)
for _, o := range data.Options {
if o.Name == "minutes" {
minutes = o.IntValue()
@@ -486,13 +607,32 @@ func onInteractionCreate(_ string) func(s *discordgo.Session, i *discordgo.Inter
if minutes < 1 {
minutes = 1
}
cfgMu.Lock()
log.Printf("Setting timeout for %d minutes", minutes)
// speichern
getCfg(guildID).TimeoutMin = int(minutes)
cfgMu.Unlock()
if err := saveGuildCfgs(); err != nil {
log.Printf("Speichern fehlgeschlagen: %v", err)
log.Printf("saveGuildCfgs failed: %v", err)
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "⚠️ Timeout gesetzt, aber Speichern fehlgeschlagen. Schau die Logs.",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: fmt.Sprintf("✅ Timeout auf %d Minuten gesetzt.", minutes), Flags: discordgo.MessageFlagsEphemeral}})
// Antwort <= 3s
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("✅ Timeout auf %d Minuten gesetzt.", minutes),
Flags: discordgo.MessageFlagsEphemeral,
},
})
case "adduser":
// wer ruft auf?
requester := i.User
@@ -589,6 +729,27 @@ func onInteractionCreate(_ string) func(s *discordgo.Session, i *discordgo.Inter
}
}
// Handler: Commands registrieren, wenn Bot auf neuen Server kommt
func onGuildCreate(s *discordgo.Session, g *discordgo.GuildCreate) {
appID := s.State.User.ID
log.Printf("Registriere Commands in neuer Guild: %s (%s)", g.Name, g.ID)
for _, cmd := range slashCommands {
c, err := s.ApplicationCommandCreate(appID, g.ID, cmd)
if err != nil {
log.Printf("Command-Registrierung in %s fehlgeschlagen: %v", g.Name, err)
continue
}
// Commands für die Guild speichern
// Wir verwenden sync.Map, also müssen wir die 'Load' und 'Store' Methoden verwenden.
commands, _ := createdCmds.LoadOrStore(g.ID, []*discordgo.ApplicationCommand{})
commands = append(commands.([]*discordgo.ApplicationCommand), c)
createdCmds.Store(g.ID, commands)
log.Printf("Registriere Command %s in Guild %s", cmd.Name, g.Name)
}
}
// ===== Commands Definition =====
var (
adminPerm = int64(discordgo.PermissionAdministrator)
@@ -643,14 +804,16 @@ var (
// ===== main: Multi-Guild, pro Guild registrieren =====
func main() {
initDB()
token := os.Getenv("DISCORD_TOKEN")
token = "MTQwMzg1MTM5NDQ1MjI5MTU4NA.GVi04l.qjraLIbFdi_N49UcSUv_BqK89ihb6xXY648J7A"
if token == "" {
log.Fatal("Bitte setze DISCORD_TOKEN")
}
// Persistente Konfiguration laden (optional)
if err := loadGuildCfgs(); err != nil {
log.Printf("Hinweis: Konnte %s nicht laden (%v). Starte mit Defaults.", configPath, err)
log.Printf("Hinweis: Konnte %s nicht laden (%v). Starte mit Defaults.", dbPath, err)
}
s, err := discordgo.New("Bot " + token)
@@ -661,25 +824,8 @@ func main() {
s.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildVoiceStates
s.AddHandler(onVoiceStateUpdate)
s.AddHandler(onGuildCreate)
s.AddHandler(onInteractionCreate(""))
s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) {
log.Printf("Eingeloggt als %s", r.User.Username)
registerOnce.Do(func() {
appID := s.State.User.ID
for _, g := range s.State.Guilds {
log.Printf("Registriere Commands in Guild: %s (%s)", g.Name, g.ID)
for _, cmd := range slashCommands {
c, err := s.ApplicationCommandCreate(appID, g.ID, cmd)
if err != nil {
log.Printf("Command-Registrierung in %s fehlgeschlagen: %v", g.Name, err)
continue
}
createdCmds[g.ID] = append(createdCmds[g.ID], c)
}
}
log.Printf("Defaults → Lobby: %q | Kategorie: %q | Timeout: %d min", defaultLobbyName, defaultCategory, envTimeoutDefault(60))
})
})
if err := s.Open(); err != nil {
log.Fatalf("Gateway-Start fehlgeschlagen: %v", err)
@@ -692,15 +838,75 @@ func main() {
log.Println("Fahre herunter…")
appID := s.State.User.ID
for guildID, cmds := range createdCmds {
for _, c := range cmds {
if delErr := s.ApplicationCommandDelete(appID, guildID, c.ID); delErr != nil {
log.Printf("Cmd-Delete (%s/%s) fehlgeschlagen: %v", guildID, c.Name, delErr)
createdCmds.Range(func(key, value interface{}) bool {
for _, c := range value.([]*discordgo.ApplicationCommand) {
if delErr := s.ApplicationCommandDelete(appID, key.(string), c.ID); delErr != nil {
log.Printf("Cmd-Delete (%s / %s) fehlgeschlagen: %v", key, c.Name, delErr)
} else {
log.Printf("Cmd-Delete (%s / %s) ok", key, c.Name)
}
}
}
return true
})
if err := saveGuildCfgs(); err != nil {
log.Printf("Speichern zum Shutdown fehlgeschlagen: %v", err)
}
_ = s.Close()
}
// Die Struktur für die Übersetzungen.
type TranslationsStruct struct {
Language string `json:"language"`
Messages map[string]string `json:"messages"`
}
// Temporäre Struktur zur Deserialisierung.
type TranslationFile struct {
Translations []TranslationsStruct `json:"translations"`
}
// Globale Variable für die Übersetzungen
var translations map[string]map[string]string
// Funktion zur Deserialisierung und Umwandlung in map[string]map[string]string
func loadTranslations(jsonData string) (map[string]map[string]string, error) {
var translationFile TranslationFile
// Deserialisierung des JSON in die Struktur
if err := json.Unmarshal([]byte(jsonData), &translationFile); err != nil {
return nil, err
}
// Umwandlung der Struktur in eine Map
result := make(map[string]map[string]string)
for _, trans := range translationFile.Translations {
result[trans.Language] = trans.Messages
}
return result, nil
}
// Funktion zur Deserialisierung und Umwandlung in map[string]map[string]string
func loadTranslationsFromFile(filename string) (map[string]map[string]string, error) {
// Dateiinhalt lesen
fileData, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("fehler beim lesen der datei: %w", err)
}
var translationFile TranslationFile
// Deserialisierung des JSON in die Struktur
if err := json.Unmarshal(fileData, &translationFile); err != nil {
return nil, fmt.Errorf("fehler beim deserialisieren der json-daten: %w", err)
}
// Umwandlung der Struktur in eine Map
result := make(map[string]map[string]string)
for _, trans := range translationFile.Translations {
result[trans.Language] = trans.Messages
}
return result, nil
}