@@ -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
}