From e3bf24c66eb87772ed33c0c096eab60d79abdba1 Mon Sep 17 00:00:00 2001 From: jbergner Date: Mon, 11 Aug 2025 08:30:05 +0200 Subject: [PATCH] =?UTF-8?q?l=C3=A4uft?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 3 +- go.mod | 15 +- go.sum | 50 +++++- guild_config.db | Bin 0 -> 12288 bytes guild_config.json | 1 + language.json | 20 +++ main.go | 420 ++++++++++++++++++++++++++++++++++------------ 7 files changed, 399 insertions(+), 110 deletions(-) create mode 100644 guild_config.db create mode 100644 guild_config.json create mode 100644 language.json diff --git a/Dockerfile b/Dockerfile index 9bcbb73..0565a97 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,8 @@ ENV LOBBY_CHANNEL_ID=0 \ DISCORD_TOKEN=0 \ GUILD_ID=0 \ CATEGORY_ID=0 \ - TIMEOUT_MIN=1 + TIMEOUT_MIN=1 \ + CONFIG_PATH=/data/guild_config.json diff --git a/go.mod b/go.mod index a4ee56d..e152655 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,21 @@ go 1.24.4 require github.com/bwmarrin/discordgo v0.29.0 +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + modernc.org/libc v1.66.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) + require ( github.com/gorilla/websocket v1.4.2 // indirect golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect - golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect + golang.org/x/sys v0.34.0 // indirect + modernc.org/sqlite v1.38.2 ) diff --git a/go.sum b/go.sum index 8e8eb37..2b9e0c7 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,60 @@ github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno= github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= +modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= +modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= +modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= +modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/guild_config.db b/guild_config.db new file mode 100644 index 0000000000000000000000000000000000000000..88142dc7a80b183db61fdc1ad3a383e0d1c92c06 GIT binary patch literal 12288 zcmeI$ze~eF6bJCT)L$)>2!fr8w-hXhvNhdyhc!+3wEII-cNc6vcrnm`>ISrR0DyLWm+WCvzTD zZkFbSe5UoHP{{7XMp6AFidrUWS$$pf0_G5a00bZa0SG_<0uX=z1Rwx`Ul+LBqN}A! zh2B%)_I*B1Jb&m6qHyGmvwP*pG7Z~gwsF`oS$4v<3;F!4;SHH>Uf8T_wN4DH$J%CZ zM-Kg{-@kQ2H{jE8+8VfmkE8foS9k%B65#}1$Xchid2CvRe6^{pl-dYAqMHLba?$*R$Ke|ltQJQ3XYIlGL#Yk8*-GRsYZ?yO+e+jA0dr6B){svkry i1Oy-e0SG_<0uX=z1Rwwb2tWV=|6O1+r{{mTDEI;cA8e5T literal 0 HcmV?d00001 diff --git a/guild_config.json b/guild_config.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/guild_config.json @@ -0,0 +1 @@ +{} diff --git a/language.json b/language.json new file mode 100644 index 0000000..8f82f34 --- /dev/null +++ b/language.json @@ -0,0 +1,20 @@ +{ + "translations": [ + { + "language": "de", + "messages": { + "system.default.lobbyname": "➕ Erstelle privaten Raum", + "system.default.category": "Private Räume", + "system.log.initdb.01": "Datenbank-Fehler: %v", + "system.log.initdb.02": "Fehler beim Erstellen der Tabelle: %v" + } + }, + { + "language": "en", + "messages": { + "hello": "Hallo, %s!", + "goodbye": "Tschüss, %s!" + } + } + ] +} \ No newline at end of file diff --git a/main.go b/main.go index 9f3ec60..f928dcb 100644 --- a/main.go +++ b/main.go @@ -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 +}