Files
discord-auto-voice/main.go
groot 627ddf762e
Some checks failed
release-tag / release-image (push) Failing after 53s
main.go aktualisiert
2025-08-10 19:58:39 +00:00

880 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"database/sql"
"fmt"
"log"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/bwmarrin/discordgo"
_ "modernc.org/sqlite" // SQLite3-Treiber importieren
)
// ===== Defaults / Names (pro Guild per Commands konfigurierbar) =====
const (
pollInterval = 15 * time.Second
defaultLobbyName = " Erstelle privaten Raum"
defaultCategory = "Private Räume"
)
// Pfad zur SQLite-Datenbank
var dbPath = func() string {
if v := os.Getenv("DB_PATH"); v != "" {
return v
}
return "guild_config.db"
}()
// Pfad zur Persistenz-Datei (überschreibbar via CONFIG_PATH)
/*var configPath = func() string {
if v := os.Getenv("CONFIG_PATH"); v != "" {
return v
}
return "guild_config.json"
}()*/
// ===== Per-Guild Config (in-memory) =====
type GuildConfig struct {
LobbyName string `json:"lobby_name"`
CategoryName string `json:"category_name"`
TimeoutMin int `json:"timeout_min"`
}
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 {
if cfg, ok := guildCfgs.Load(guildID); ok {
return cfg.(*GuildConfig)
}
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.Store(guildID, &guildCfg)
return &guildCfg
}
func envTimeoutDefault(def int) int {
if v := os.Getenv("TIMEOUT_MIN"); v != "" {
var x int
if _, err := fmt.Sscanf(v, "%d", &x); err == nil && x > 0 {
return x
}
}
return def
}
// ===== Helpers: Channel/Kategorie finden =====
func findOrCreateCategoryID(s *discordgo.Session, guildID, name string) (string, error) {
chans, err := s.GuildChannels(guildID)
if err != nil {
return "", err
}
for _, ch := range chans {
if ch.Type == discordgo.ChannelTypeGuildCategory && ch.Name == name {
return ch.ID, nil
}
}
cat, err := s.GuildChannelCreateComplex(guildID, discordgo.GuildChannelCreateData{
Name: name,
Type: discordgo.ChannelTypeGuildCategory,
})
if err != nil {
return "", err
}
return cat.ID, nil
}
func findVoiceChannelIDByName(s *discordgo.Session, guildID, name string) string {
chans, err := s.GuildChannels(guildID)
if err != nil {
return ""
}
for _, ch := range chans {
if ch.Type == discordgo.ChannelTypeGuildVoice && ch.Name == name {
return ch.ID
}
}
return ""
}
func findUserVoiceChannelID(s *discordgo.Session, guildID, userID string) string {
g, err := s.State.Guild(guildID)
if err != nil {
g, err = s.Guild(guildID)
if err != nil {
return ""
}
}
for _, vs := range g.VoiceStates {
if vs.UserID == userID && vs.ChannelID != "" {
return vs.ChannelID
}
}
return ""
}
// ===== Anzeige-Helper =====
func safeDisplayName(m *discordgo.Member) string {
if m == nil {
return "privat"
}
if m.Nick != "" {
return m.Nick
}
if m.User.GlobalName != "" {
return m.User.GlobalName
}
return m.User.Username
}
func displayName(i *discordgo.InteractionCreate) string {
if i.Member != nil && i.Member.Nick != "" {
return i.Member.Nick
}
if i.Member != nil && i.Member.User.GlobalName != "" {
return i.Member.User.GlobalName
}
if i.Member != nil {
return i.Member.User.Username
}
if i.User != nil && i.User.GlobalName != "" {
return i.User.GlobalName
}
if i.User != nil {
return i.User.Username
}
return "privat"
}
// ===== Unified Create + Move + Cleanup =====
func createPrivateVCAndMove(
s *discordgo.Session,
guildID, requesterID, disp, categoryID string,
userLimit, timeoutMin int,
srcChannelID string, // Quelle für Move (Lobby/aktueller VC); leer = kein Move
) (*discordgo.Channel, error) {
botID := s.State.User.ID
name := "🔒 " + disp
allowOwner := int64(
discordgo.PermissionViewChannel |
discordgo.PermissionVoiceConnect |
discordgo.PermissionVoiceSpeak |
discordgo.PermissionVoiceUseVAD |
discordgo.PermissionVoiceStreamVideo |
discordgo.PermissionManageChannels,
)
denyEveryone := int64(
discordgo.PermissionViewChannel |
discordgo.PermissionVoiceConnect,
)
allowBot := int64(
discordgo.PermissionViewChannel |
discordgo.PermissionVoiceConnect |
discordgo.PermissionVoiceMoveMembers |
discordgo.PermissionManageChannels,
)
newChan, err := s.GuildChannelCreateComplex(guildID, discordgo.GuildChannelCreateData{
Name: name,
Type: discordgo.ChannelTypeGuildVoice,
ParentID: categoryID,
UserLimit: userLimit,
Bitrate: 64000,
PermissionOverwrites: []*discordgo.PermissionOverwrite{
{ID: guildID, Type: discordgo.PermissionOverwriteTypeRole, Deny: denyEveryone},
{ID: requesterID, Type: discordgo.PermissionOverwriteTypeMember, Allow: allowOwner},
{ID: botID, Type: discordgo.PermissionOverwriteTypeMember, Allow: allowBot},
},
})
if err != nil {
return nil, fmt.Errorf("VC-Anlage fehlgeschlagen: %w", err)
}
// optionaler Move
if srcChannelID != "" {
go func() {
time.Sleep(200 * time.Millisecond)
// Best-effort Perm Checks
if perms, err := s.UserChannelPermissions(botID, srcChannelID); err == nil {
need := int64(discordgo.PermissionVoiceMoveMembers)
if perms&need == 0 {
log.Printf("WARN: Bot hat im Quell-Channel keine MoveMembers-Permission")
}
}
if perms, err := s.UserChannelPermissions(botID, newChan.ID); err == nil {
need := int64(discordgo.PermissionVoiceMoveMembers)
if perms&need == 0 {
log.Printf("WARN: Bot hat im Ziel-Channel keine MoveMembers-Permission")
}
}
var moveErr error
for attempt := 0; attempt < 5; attempt++ {
moveErr = s.GuildMemberMove(guildID, requesterID, &newChan.ID)
if moveErr == nil {
break
}
wait := time.Duration(150*(attempt+1)) * time.Millisecond
log.Printf("Move fehlgeschlagen (Versuch %d): %v - retry in %s", attempt+1, moveErr, wait)
time.Sleep(wait)
}
if moveErr != nil {
log.Printf("Move endgültig fehlgeschlagen: %v", moveErr)
}
}()
}
go watchAndCleanup(s, guildID, newChan.ID, time.Duration(timeoutMin)*time.Minute)
log.Println(" Added channel for guildID: " + guildID + " with new ID: " + newChan.ID)
return newChan, nil
}
// ===== Auto-Cleanup =====
func watchAndCleanup(s *discordgo.Session, guildID, channelID string, timeout time.Duration) {
lastActive := time.Now()
ticker := time.NewTicker(pollInterval)
defer ticker.Stop()
for range ticker.C {
g, err := s.State.Guild(guildID)
if err != nil {
g, err = s.Guild(guildID)
if err != nil {
continue
}
}
occupied := false
for _, vs := range g.VoiceStates {
if vs.ChannelID == channelID {
occupied = true
break
}
}
if occupied {
lastActive = time.Now()
continue
}
if time.Since(lastActive) >= timeout {
_, _ = s.ChannelDelete(channelID)
log.Println(" Deleted channel for guildID: " + guildID + " with ID: " + channelID)
return
}
}
}
// ===== Handlers =====
// Lobby-Join → VC erstellen & move (per Namen aus GuildConfig)
func onVoiceStateUpdate(s *discordgo.Session, e *discordgo.VoiceStateUpdate) {
if e.UserID == "" {
return
}
cfg := getCfg(e.GuildID)
lobby := findVoiceChannelIDByName(s, e.GuildID, cfg.LobbyName)
if lobby == "" || e.ChannelID != lobby || (e.BeforeUpdate != nil && e.BeforeUpdate.ChannelID == lobby) {
return
}
m, err := s.GuildMember(e.GuildID, e.UserID)
if err == nil && m.User.Bot {
return
}
catID, err := findOrCreateCategoryID(s, e.GuildID, cfg.CategoryName)
if err != nil {
log.Printf("Kategorie-Auflösung fehlgeschlagen: %v", err)
return
}
_, err = createPrivateVCAndMove(s, e.GuildID, e.UserID, safeDisplayName(m), catID, 0, cfg.TimeoutMin, lobby)
if err != nil {
log.Printf("VC-Erstellung/Move fehlgeschlagen: %v", err)
}
}
// ===== Permissions Helper =====
func isGuildAdmin(s *discordgo.Session, guildID string, user *discordgo.User, member *discordgo.Member) bool {
if user == nil && member == nil {
return false
}
if member == nil {
m, err := s.GuildMember(guildID, user.ID)
if err != nil {
return false
}
member = m
}
g, err := s.State.Guild(guildID)
if err == nil && g.OwnerID == member.User.ID {
return true
}
roles, err := s.GuildRoles(guildID)
if err != nil {
return false
}
rolePerm := int64(0)
for _, r := range roles {
for _, mr := range member.Roles {
if r.ID == mr {
rolePerm |= r.Permissions
}
}
}
if rolePerm&int64(discordgo.PermissionAdministrator) != 0 {
return true
}
if rolePerm&int64(discordgo.PermissionManageGuild) != 0 { // Fallback: Manage Server reicht auch
return true
}
return false
}
// Slash-Commands: makevc / setlobby / setcategory / settimeout (Admin-geschützt)
func onInteractionCreate(_ string) func(s *discordgo.Session, i *discordgo.InteractionCreate) {
return func(s *discordgo.Session, i *discordgo.InteractionCreate) {
if i.Type != discordgo.InteractionApplicationCommand {
return
}
data := i.ApplicationCommandData()
guildID := i.GuildID
switch data.Name {
case "makevc":
cfg := getCfg(guildID)
user := i.User
if user == nil && i.Member != nil {
user = i.Member.User
}
if user == nil {
return
}
userLimit := 0
timeoutMin := cfg.TimeoutMin
var nameOpt string
for _, o := range data.Options {
switch o.Name {
case "name":
nameOpt = o.StringValue()
case "user_limit":
userLimit = int(o.IntValue())
case "timeout_min":
timeoutMin = int(o.IntValue())
}
}
display := displayName(i)
if nameOpt != "" {
display = nameOpt
}
catID, err := findOrCreateCategoryID(s, guildID, cfg.CategoryName)
if err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "Konnte Kategorie nicht finden/erstellen.", Flags: discordgo.MessageFlagsEphemeral}})
return
}
srcVC := findUserVoiceChannelID(s, guildID, user.ID)
newChan, err := createPrivateVCAndMove(s, guildID, user.ID, display, catID, userLimit, timeoutMin, srcVC)
if err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{Content: "Konnte Voice-Channel nicht erstellen. Fehlen dem Bot Rechte (Manage Channels/Move Members)?", Flags: discordgo.MessageFlagsEphemeral}})
return
}
msg := fmt.Sprintf("✅ Voice-Channel **%s** erstellt.", newChan.Name)
if srcVC == "" {
msg += " Du bist aktuell in keinem Voice-Channel; join bitte manuell in deinen neuen Raum."
} else {
msg += " Ich verschiebe dich jetzt dort hinein."
}
_ = 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,
},
})
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,
},
})
return
}
// speichern
getCfg(guildID).LobbyName = name
if err := saveGuildCfgs(); err != nil {
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
}
// 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,
},
})
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,
},
})
return
}
// speichern
getCfg(guildID).CategoryName = name
if err := saveGuildCfgs(); err != nil {
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
}
// 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) {
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
}
minutes := int64(getCfg(guildID).TimeoutMin)
for _, o := range data.Options {
if o.Name == "minutes" {
minutes = o.IntValue()
}
}
if minutes < 1 {
minutes = 1
}
log.Printf("Setting timeout for %d minutes", minutes)
// speichern
getCfg(guildID).TimeoutMin = int(minutes)
log.Println("1-0815")
if err := saveGuildCfgs(); err != nil {
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
}
log.Println("2-0815")
// 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
if requester == nil && i.Member != nil {
requester = i.Member.User
}
if requester == nil {
return
}
// Ziel-User aus den Optionen holen
var target *discordgo.User
for _, o := range data.Options {
if o.Name == "user" {
// UserValue braucht die Session
u := o.UserValue(s)
if u != nil {
target = u
}
}
}
if target == nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Bitte gib ein gültiges Mitglied an.",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
// In welchem VC ist der Aufrufer gerade?
srcVC := findUserVoiceChannelID(s, guildID, requester.ID)
if srcVC == "" {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Du bist aktuell in keinem Voice-Channel. Betritt zuerst deinen privaten Channel.",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
// Sicherheits-Check: darf der Aufrufer diesen Channel verwalten?
// (Der Owner bekommt in deinem Code ManageChannels → das ist unser Indikator)
perms, err := s.UserChannelPermissions(requester.ID, srcVC)
if err != nil || perms&int64(discordgo.PermissionManageChannels) == 0 {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "❌ Du bist nicht der Besitzer dieses Channels oder dir fehlen Rechte (Manage Channel).",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
// Overwrite setzen: Ziel-User darf sehen & beitreten (& sprechen/Stream/VAD)
allow := int64(
discordgo.PermissionViewChannel |
discordgo.PermissionVoiceConnect |
discordgo.PermissionVoiceSpeak |
discordgo.PermissionVoiceUseVAD |
discordgo.PermissionVoiceStreamVideo,
)
if err := s.ChannelPermissionSet(
srcVC,
target.ID,
discordgo.PermissionOverwriteTypeMember,
allow,
0,
); err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Konnte Berechtigung nicht setzen: %v", err),
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("✅ %s hat jetzt Zugriff auf deinen Voice-Channel.", target.Username),
Flags: discordgo.MessageFlagsEphemeral,
},
})
}
}
}
// 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("Command-Registrierung in %s", g.Name)
}
}
// ===== Commands Definition =====
var (
adminPerm = int64(discordgo.PermissionAdministrator)
slashCommands = []*discordgo.ApplicationCommand{
{
Name: "makevc",
Description: "Erstellt einen privaten Voice-Channel (Auto-Cleanup)",
Options: []*discordgo.ApplicationCommandOption{
{Type: discordgo.ApplicationCommandOptionString, Name: "name", Description: "Name des Channels", Required: false},
{Type: discordgo.ApplicationCommandOptionInteger, Name: "user_limit", Description: "Max. Nutzer (0=unbegrenzt)", Required: false, MaxValue: 99},
{Type: discordgo.ApplicationCommandOptionInteger, Name: "timeout_min", Description: "Löschen nach X Minuten Inaktivität", Required: false, MaxValue: 480},
},
},
{
Name: "setlobby",
Description: "Setzt den Namen des Lobby-Voice-Channels",
DefaultMemberPermissions: &adminPerm,
Options: []*discordgo.ApplicationCommandOption{
{Type: discordgo.ApplicationCommandOptionString, Name: "name", Description: "Neuer Lobby-Name", Required: true},
},
},
{
Name: "setcategory",
Description: "Setzt den Namen der Kategorie für private Räume",
DefaultMemberPermissions: &adminPerm,
Options: []*discordgo.ApplicationCommandOption{
{Type: discordgo.ApplicationCommandOptionString, Name: "name", Description: "Neue Kategorie-Bezeichnung", Required: true},
},
},
{
Name: "settimeout",
Description: "Setzt das Auto-Lösch-Timeout (Minuten)",
DefaultMemberPermissions: &adminPerm,
Options: []*discordgo.ApplicationCommandOption{
{Type: discordgo.ApplicationCommandOptionInteger, Name: "minutes", Description: "Minuten (>=1)", Required: true, MaxValue: 480},
},
},
{
Name: "adduser",
Description: "Gibt einem Mitglied Zugriff auf deinen aktuellen privaten Voice-Channel",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionUser,
Name: "user",
Description: "Mitglied, das Zugriff bekommen soll",
Required: true,
},
},
},
}
)
/*func registerCommandsForGuild(s *discordgo.Session, guildID string) {
appID := s.State.User.ID
cmds, err := s.ApplicationCommandBulkOverwrite(appID, guildID, slashCommands)
if err != nil {
log.Printf("BulkOverwrite %s failed: %v", guildID, err)
return
}
cfgMu.Lock()
createdCmds[guildID] = cmds
cfgMu.Unlock()
//fmt.Println(createdCmds)
}*/
// ===== 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.", dbPath, err)
}
s, err := discordgo.New("Bot " + token)
if err != nil {
log.Fatalf("Session fehlgeschlagen: %v", err)
}
s.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildVoiceStates
s.AddHandler(onVoiceStateUpdate)
s.AddHandler(onGuildCreate)
s.AddHandler(onInteractionCreate(""))
if err := s.Open(); err != nil {
log.Fatalf("Gateway-Start fehlgeschlagen: %v", err)
}
log.Println("Bot online. Ctrl+C zum Beenden.")
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
log.Println("Fahre herunter…")
appID := s.State.User.ID
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()
}