Files
discord-auto-voice/main.go
groot 39b5c86d40
All checks were successful
release-tag / release-image (push) Successful in 2m1s
main.go aktualisiert
2025-08-11 06:58:24 +00:00

1027 lines
31 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"
"encoding/json"
"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) =====
var (
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"
}()
// ===== Per-Guild Config (in-memory) =====
type GuildConfig struct {
LobbyName string `json:"lobby_name"`
CategoryName string `json:"category_name"`
TimeoutMin int `json:"timeout_min"`
Language string `json:"language"` // Neue Eigenschaft für Sprache
}
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
// Sprache für eine Guild setzen
func setLanguage(guildID, language string) error {
cfg := getCfg(guildID)
cfg.Language = language
return saveGuildCfgs()
}
// Sprachabruf
func getLanguage(guildID string) string {
cfg := getCfg(guildID)
return cfg.Language
}
// Funktion zum Hinzufügen der 'language' Spalte, falls sie nicht existiert
func addLanguageColumnIfNotExists() {
// Überprüfen, ob die Tabelle 'guild_config' existiert
var tableExists bool
err := db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='guild_config'").Scan(&tableExists)
if err != nil || !tableExists {
log.Printf("Tabelle 'guild_config' existiert nicht: %v", err)
}
// Überprüfen, ob die Spalte 'language' existiert
_, err = db.Exec("ALTER TABLE guild_config ADD COLUMN language TEXT DEFAULT 'de'")
if err != nil && !isColumnAlreadyExistsError(err) {
log.Printf("Fehler beim Hinzufügen der 'language' Spalte: %v", err)
} else {
log.Println("Spalte 'language' wurde hinzugefügt oder existiert bereits.")
}
}
// Hilfsfunktion zur Fehlerbehandlung, um zu überprüfen, ob der Fehler auf eine bereits vorhandene Spalte hinweist
func isColumnAlreadyExistsError(err error) bool {
// Fehlerbehandlung für 'duplicate column name'
return err != nil && err.Error() == "SQLITE_ERROR: duplicate column name: language"
}
// 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,
language TEXT DEFAULT 'de' -- Neue Spalte für Sprache hinzufügen
);`
_, err = db.Exec(createTableSQL)
if err != nil {
log.Fatalf("Fehler beim Erstellen der Tabelle: %v", err)
}
// Neue Spalte für die Sprache hinzufügen, wenn sie noch nicht existiert
addLanguageColumnIfNotExists()
}
// 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 mit Sprache
func loadGuildCfgs() error {
rows, err := db.Query("SELECT guild_id, lobby_name, category_name, timeout_min, language FROM guild_config")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var guildID, lobbyName, categoryName, language string
var timeoutMin int
if err := rows.Scan(&guildID, &lobbyName, &categoryName, &timeoutMin, &language); err != nil {
return err
}
guildCfgs.Store(guildID, &GuildConfig{
LobbyName: lobbyName,
CategoryName: categoryName,
TimeoutMin: timeoutMin,
Language: language, // Sprache laden
})
}
return nil
}
// Speichern der Guild-Konfiguration mit Sprache
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, language)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(guild_id)
DO UPDATE SET
lobby_name = excluded.lobby_name,
category_name = excluded.category_name,
timeout_min = excluded.timeout_min,
language = excluded.language -- Sprache speichern
`, guildID, cfg.LobbyName, cfg.CategoryName, cfg.TimeoutMin, cfg.Language)
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, language FROM guild_config WHERE guild_id = ?", guildID).Scan(&guildCfg.LobbyName, &guildCfg.CategoryName, &guildCfg.TimeoutMin, &guildCfg.Language)
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)
}
// Standardwerte verwenden
guildCfg = GuildConfig{
LobbyName: defaultLobbyName,
CategoryName: defaultCategory,
TimeoutMin: envTimeoutDefault(1),
Language: "de", // Standard-Sprache ist Deutsch
}
}
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)
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
}
// 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,
},
})
case "setlanguage":
// 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 "language" lesen
var language string
for _, o := range data.Options {
if o.Name == "language" {
language = o.StringValue()
break
}
}
if language == "" {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Bitte gib eine gültige Sprache an (z.B. 'de', 'en').",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
// Speichern
if err := setLanguage(guildID, language); err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "⚠️ Fehler beim Speichern der Sprache.",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
// Antwort <= 3s
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("✅ Sprache auf '%s' gesetzt.", language),
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("Registriere Command %s in Guild %s", cmd.Name, 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,
},
},
},
{
Name: "setlanguage",
Description: "Setzt die Sprache für diese Guild.",
DefaultMemberPermissions: &adminPerm,
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "language",
Description: "Die Sprache, die gesetzt werden soll (z.B. 'de', 'en').",
Required: true,
},
},
},
}
)
// ===== 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)
}
closeDB()
_ = 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
}