From 347faf6b94b4a08887bd6976293aab652bdd2ec8 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 21 Sep 2026 18:48:11 +0200 Subject: [PATCH] Add Discord client, command router and voice channel handling --- internal/discord/client.go | 25 ++++++++++++ internal/discord/commands.go | 15 +++++++ internal/discord/router.go | 36 ++++++++++++++++ internal/discord/voice.go | 79 ++++++++++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 internal/discord/client.go create mode 100644 internal/discord/commands.go create mode 100644 internal/discord/router.go create mode 100644 internal/discord/voice.go diff --git a/internal/discord/client.go b/internal/discord/client.go new file mode 100644 index 0000000..3742bf1 --- /dev/null +++ b/internal/discord/client.go @@ -0,0 +1,25 @@ +package discord + +import ( + "log/slog" + + "github.com/disgoorg/disgo" + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/cache" + "github.com/disgoorg/disgo/gateway" +) + +func NewClient(token string, logger *slog.Logger) (*bot.Client, error) { + return disgo.New(token, + bot.WithLogger(logger), + bot.WithGatewayConfigOpts( + gateway.WithIntents( + gateway.IntentGuilds, + gateway.IntentGuildVoiceStates, + ), + ), + bot.WithCacheConfigOpts( + cache.WithCaches(cache.FlagVoiceStates), + ), + ) +} diff --git a/internal/discord/commands.go b/internal/discord/commands.go new file mode 100644 index 0000000..8c033dc --- /dev/null +++ b/internal/discord/commands.go @@ -0,0 +1,15 @@ +package discord + +import ( + "fmt" + + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/discord" +) + +func RegisterCommands(client *bot.Client, definitions []discord.ApplicationCommandCreate) error { + if _, err := client.Rest.SetGlobalCommands(client.ApplicationID, definitions); err != nil { + return fmt.Errorf("registering commands: %w", err) + } + return nil +} diff --git a/internal/discord/router.go b/internal/discord/router.go new file mode 100644 index 0000000..b9dca23 --- /dev/null +++ b/internal/discord/router.go @@ -0,0 +1,36 @@ +package discord + +import ( + "context" + "log/slog" + + "github.com/disgoorg/disgo/events" + + "discord-music-bot/internal/commands" +) + +type Router struct { + registry *commands.Registry + logger *slog.Logger +} + +func NewRouter(registry *commands.Registry, logger *slog.Logger) *Router { + return &Router{ + registry: registry, + logger: logger, + } +} + +func (r *Router) OnApplicationCommand(event *events.ApplicationCommandInteractionCreate) { + name := event.SlashCommandInteractionData().CommandName() + + cmd, ok := r.registry.Get(name) + if !ok { + r.logger.Warn("received unknown command", slog.String("command", name)) + return + } + + if err := cmd.Execute(context.Background(), event); err != nil { + r.logger.Error("command execution failed", slog.String("command", name), slog.Any("err", err)) + } +} diff --git a/internal/discord/voice.go b/internal/discord/voice.go new file mode 100644 index 0000000..e26cde2 --- /dev/null +++ b/internal/discord/voice.go @@ -0,0 +1,79 @@ +package discord + +import ( + "context" + "log/slog" + + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/events" + "github.com/disgoorg/disgolink/v4/disgolink" + + "discord-music-bot/internal/music" +) + +type VoiceForwarder struct { + client *bot.Client + lavalink *disgolink.Client + music *music.Manager + logger *slog.Logger +} + +func NewVoiceForwarder(client *bot.Client, lavalinkClient *disgolink.Client, musicManager *music.Manager, logger *slog.Logger) *VoiceForwarder { + return &VoiceForwarder{ + client: client, + lavalink: lavalinkClient, + music: musicManager, + logger: logger, + } +} + +func (f *VoiceForwarder) OnVoiceStateUpdate(event *events.GuildVoiceStateUpdate) { + if event.VoiceState.UserID == f.client.ApplicationID { + f.lavalink.OnVoiceStateUpdate(context.Background(), event.VoiceState.GuildID, event.VoiceState.ChannelID, event.VoiceState.SessionID) + return + } + + f.leaveIfNowAlone(event) +} + +func (f *VoiceForwarder) leaveIfNowAlone(event *events.GuildVoiceStateUpdate) { + oldChannelID := event.OldVoiceState.ChannelID + if oldChannelID == nil { + return + } + if newChannelID := event.VoiceState.ChannelID; newChannelID != nil && *newChannelID == *oldChannelID { + return + } + + guildID := event.VoiceState.GuildID + + botState, ok := f.client.Caches.VoiceState(guildID, f.client.ApplicationID) + if !ok || botState.ChannelID == nil || *botState.ChannelID != *oldChannelID { + return + } + + for state := range f.client.Caches.VoiceStates(guildID) { + if state.UserID != f.client.ApplicationID && state.ChannelID != nil && *state.ChannelID == *oldChannelID { + return + } + } + + ctx := context.Background() + if err := f.music.Guild(guildID).Stop(ctx); err != nil { + f.logger.ErrorContext(ctx, "failed to stop playback after being left alone", + slog.String("guild_id", guildID.String()), slog.Any("err", err)) + } + if err := f.client.UpdateVoiceState(ctx, guildID, nil, false, false); err != nil { + f.logger.ErrorContext(ctx, "failed to leave voice channel after being left alone", + slog.String("guild_id", guildID.String()), slog.Any("err", err)) + } +} + +func (f *VoiceForwarder) OnVoiceServerUpdate(event *events.VoiceServerUpdate) { + if event.Endpoint == nil { + f.logger.Warn("received voice server update without an endpoint", slog.String("guild_id", event.GuildID.String())) + return + } + + f.lavalink.OnVoiceServerUpdate(context.Background(), event.GuildID, event.Token, *event.Endpoint) +}