diff --git a/main.go b/main.go index bc42658..848b0ff 100644 --- a/main.go +++ b/main.go @@ -271,17 +271,70 @@ func envTimeoutDefault(def int) int { return def } -// ===== Helpers: Channel/Kategorie finden ===== +// Optional: Cache-Invalidierung bei Channel-Events (empfohlen) +func hookCategoryCacheInvalidation(s *discordgo.Session) { + s.AddHandler(func(_ *discordgo.Session, e *discordgo.ChannelDelete) { + if e.Channel != nil && e.Channel.Type == discordgo.ChannelTypeGuildCategory { + // alle Keys mit dieser ID rauswerfen + catCache.Range(func(k, v interface{}) bool { + if v.(cacheEntry).id == e.ID { + catCache.Delete(k) + } + return true + }) + } + }) + s.AddHandler(func(_ *discordgo.Session, e *discordgo.ChannelUpdate) { + if e.Channel != nil && e.Channel.Type == discordgo.ChannelTypeGuildCategory { + // Bei Rename nicht perfekt zu erkennen -> TTL sorgt für Refresh + // Optional: hart löschen: + catCache.Range(func(k, v interface{}) bool { + if v.(cacheEntry).id == e.Channel.ID { + catCache.Delete(k) + } + return true + }) + } + }) +} + +// ===== Helpers: Channel/Kategorie finden (mit Cache) ===== 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 + key := guildID + "|" + name + + // 0) Cache + if v, ok := catCache.Load(key); ok { + ce := v.(cacheEntry) + if time.Since(ce.t) < cacheTTL { + return ce.id, nil } } + + // 1) State (schnell) – vorausgesetzt: s.State.TrackChannels = true (vor s.Open() setzen!) + if g, err := s.State.Guild(guildID); err == nil { + for _, ch := range g.Channels { + if ch.Type == discordgo.ChannelTypeGuildCategory && ch.Name == name { + catCache.Store(key, cacheEntry{ch.ID, time.Now()}) + return ch.ID, nil + } + } + } + + // 2) REST (Fallback) + chans, err := s.GuildChannels(guildID) + if err == nil { + for _, ch := range chans { + if ch.Type == discordgo.ChannelTypeGuildCategory && ch.Name == name { + catCache.Store(key, cacheEntry{ch.ID, time.Now()}) + return ch.ID, nil + } + } + } else { + // wenn selbst Channels holen fehlschlägt, gib den Fehler zurück + return "", err + } + + // 3) Nicht gefunden -> anlegen cat, err := s.GuildChannelCreateComplex(guildID, discordgo.GuildChannelCreateData{ Name: name, Type: discordgo.ChannelTypeGuildCategory, @@ -289,30 +342,76 @@ func findOrCreateCategoryID(s *discordgo.Session, guildID, name string) (string, if err != nil { return "", err } + catCache.Store(key, cacheEntry{cat.ID, time.Now()}) return cat.ID, nil } +var ( + vcCache sync.Map // key: guildID+"|"+name → cacheEntry + catCache sync.Map // key: guildID+"|"+name -> cacheEntry + cacheTTL = 12 * time.Hour +) + +type cacheEntry struct { + id string + t time.Time +} + +// Schneller: erst State, dann REST, plus kleiner TTL-Cache func findVoiceChannelIDByName(s *discordgo.Session, guildID, name string) string { + key := guildID + "|" + name + if v, ok := vcCache.Load(key); ok { + ce := v.(cacheEntry) + if time.Since(ce.t) < cacheTTL { + return ce.id + } + } + + // 1) State (sehr schnell) + if g, err := s.State.Guild(guildID); err == nil { + for _, ch := range g.Channels { + if ch.Type == discordgo.ChannelTypeGuildVoice && ch.Name == name { + vcCache.Store(key, cacheEntry{ch.ID, time.Now()}) + return ch.ID + } + } + } + + // 2) REST (Fallback, teuer) chans, err := s.GuildChannels(guildID) if err != nil { return "" } for _, ch := range chans { if ch.Type == discordgo.ChannelTypeGuildVoice && ch.Name == name { + vcCache.Store(key, cacheEntry{ch.ID, time.Now()}) return ch.ID } } return "" } +// Noch schneller: gezielt VoiceState aus dem State ziehen 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 "" + // 1) Direkt per VoiceState (O(1), wenn im State vorhanden) + if vs, err := s.State.VoiceState(guildID, userID); err == nil && vs != nil && vs.ChannelID != "" { + return vs.ChannelID + } + + // 2) State.Guild als Fallback (O(n), aber ohne REST) + if g, err := s.State.Guild(guildID); err == nil { + for _, vs := range g.VoiceStates { + if vs.UserID == userID && vs.ChannelID != "" { + return vs.ChannelID + } } } + + // 3) REST (letzter Ausweg) + g, err := s.Guild(guildID) + if err != nil { + return "" + } for _, vs := range g.VoiceStates { if vs.UserID == userID && vs.ChannelID != "" { return vs.ChannelID @@ -1179,6 +1278,14 @@ func main() { s.AddHandler(onVoiceStateUpdate) s.AddHandler(onGuildCreate) s.AddHandler(onInteractionCreate("")) + s.AddHandler(func(_ *discordgo.Session, e *discordgo.ChannelDelete) { + vcCache.Range(func(k, v interface{}) bool { + if v.(cacheEntry).id == e.ID { + vcCache.Delete(k) + } + return true + }) + }) if err := s.Open(); err != nil { log.Fatalf("Gateway-Start fehlgeschlagen: %v", err)