From 01dbb98450dd68db6f0fc09d55de63f381e64c36 Mon Sep 17 00:00:00 2001 From: FernandoJVideira <03.pleaser-minster@icloud.com> Date: Tue, 18 Aug 2026 21:20:31 +0100 Subject: [PATCH] Command rework --- internal/commands/commands.go | 4 + .../commands/projects/public/components.go | 222 +++++++++++++++ internal/commands/projects/public/projects.go | 261 ++++++++++-------- internal/commands/projects/registry.go | 6 + internal/db/repos/projectsrepo/repo.go | 60 ++++ internal/db/repos/twitchrepo/repo.go | 28 ++ internal/db/services/projects/service.go | 12 + internal/db/services/twitch/service.go | 8 + internal/events/twitch_live.go | 10 + internal/music/components.go | 5 +- schema.sql | 3 +- 11 files changed, 502 insertions(+), 117 deletions(-) create mode 100644 internal/commands/projects/public/components.go diff --git a/internal/commands/commands.go b/internal/commands/commands.go index afd0727..13d5bb0 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -78,12 +78,16 @@ func HandleInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) { music.HandleComponent(s, i) case strings.HasPrefix(data.CustomID, "schedule:"): schedule.HandleComponent(s, i) + case strings.HasPrefix(data.CustomID, "projects:"): + projects.HandleComponent(s, i) } case discordgo.InteractionModalSubmit: data := i.ModalSubmitData() switch { case strings.HasPrefix(data.CustomID, "schedule:"): schedule.HandleModalSubmit(s, i) + case strings.HasPrefix(data.CustomID, "projects:"): + projects.HandleModalSubmit(s, i) } } diff --git a/internal/commands/projects/public/components.go b/internal/commands/projects/public/components.go new file mode 100644 index 0000000..a36bd0d --- /dev/null +++ b/internal/commands/projects/public/components.go @@ -0,0 +1,222 @@ +package public + +import ( + "context" + "strconv" + "strings" + + "velox-bot/internal/commands/projects/shared" + "velox-bot/internal/db/services" + + "github.com/bwmarrin/discordgo" +) + +// HandleComponent routes the select menus shown after /projects add-helper +// and /projects close. +func HandleComponent(s *discordgo.Session, i *discordgo.InteractionCreate) { + parts := strings.Split(i.MessageComponentData().CustomID, ":") + if len(parts) < 2 { + return + } + + switch parts[1] { + case "add_helper_project_select": + handleAddHelperProjectSelect(s, i) + case "add_helper_user_select": + handleAddHelperUserSelect(s, i, parts) + case "close_select": + handleCloseSelect(s, i) + } +} + +// handleAddHelperProjectSelect is step 1: a project was picked, now show a +// native Discord user-select to pick who helps - the actual AddHelper call +// happens in handleAddHelperUserSelect once both are known. +func handleAddHelperProjectSelect(s *discordgo.Session, i *discordgo.InteractionCreate) { + if !shared.RequireManageGuild(s, i) { + return + } + + values := i.MessageComponentData().Values + if len(values) == 0 { + return + } + projectID := values[0] + + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Content: "Who should help with this project?", + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.SelectMenu{ + MenuType: discordgo.UserSelectMenu, + CustomID: "projects:add_helper_user_select:" + projectID, + Placeholder: "Select a user", + }, + }, + }, + }, + }, + }) +} + +// handleAddHelperUserSelect is step 2: a user was picked from the native +// select menu, so both the project (carried in the CustomID) and the user +// (the selected value) are now known - perform the actual AddHelper call. +func handleAddHelperUserSelect(s *discordgo.Session, i *discordgo.InteractionCreate, parts []string) { + if !shared.RequireManageGuild(s, i) { + return + } + if len(parts) < 3 { + return + } + projectID, err := strconv.ParseInt(parts[2], 10, 64) + if err != nil { + return + } + + guildID, ok := shared.ParseGuildID(s, i) + if !ok { + return + } + + values := i.MessageComponentData().Values + if len(values) == 0 { + return + } + userID := values[0] + userID64, err := strconv.ParseInt(userID, 10, 64) + if err != nil { + return + } + + ctx := context.Background() + project, err := services.Global.Projects.GetProject(ctx, guildID, projectID) + if err != nil { + shared.RespondEphemeral(s, i, "Failed to load project.") + return + } + if project == nil { + shared.RespondEphemeral(s, i, "That project no longer exists in this server.") + return + } + + if err := services.Global.Projects.AddHelper(ctx, projectID, userID64); err != nil { + shared.RespondEphemeral(s, i, "Failed to add helper to project.") + return + } + + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Content: "Added <@" + userID + "> as helper to project **" + project.Name + "**.", + Components: []discordgo.MessageComponent{}, + }, + }) +} + +func handleCloseSelect(s *discordgo.Session, i *discordgo.InteractionCreate) { + if !shared.RequireManageGuild(s, i) { + return + } + + guildID, ok := shared.ParseGuildID(s, i) + if !ok { + return + } + + values := i.MessageComponentData().Values + if len(values) == 0 { + return + } + projectID, err := strconv.ParseInt(values[0], 10, 64) + if err != nil { + return + } + + ctx := context.Background() + project, err := services.Global.Projects.GetProject(ctx, guildID, projectID) + if err != nil { + shared.RespondEphemeral(s, i, "Failed to load project.") + return + } + if project == nil { + shared.RespondEphemeral(s, i, "That project no longer exists in this server.") + return + } + + if err := services.Global.Projects.DeactivateProject(ctx, guildID, projectID); err != nil { + shared.RespondEphemeral(s, i, "Failed to close project.") + return + } + + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Content: "Closed project **" + project.Name + "**.", + Components: []discordgo.MessageComponent{}, + }, + }) +} + +// HandleModalSubmit handles the /projects create modal. +func HandleModalSubmit(s *discordgo.Session, i *discordgo.InteractionCreate) { + parts := strings.Split(i.ModalSubmitData().CustomID, ":") + if len(parts) < 2 || parts[1] != "create_modal" { + return + } + + if !shared.RequireManageGuild(s, i) { + return + } + guildID, ok := shared.ParseGuildID(s, i) + if !ok { + return + } + if i.Member == nil || i.Member.User == nil { + shared.RespondEphemeral(s, i, "Missing member info.") + return + } + + values := modalTextInputValues(i.ModalSubmitData().Components) + name := strings.TrimSpace(values["name"]) + description := strings.TrimSpace(values["description"]) + if name == "" { + shared.RespondEphemeral(s, i, "Project name cannot be empty.") + return + } + + creatorID, err := strconv.ParseInt(i.Member.User.ID, 10, 64) + if err != nil { + shared.RespondEphemeral(s, i, "Invalid user ID.") + return + } + + id, err := services.Global.Projects.CreateProject(context.Background(), guildID, creatorID, name, description) + if err != nil { + shared.RespondEphemeral(s, i, "Failed to create project.") + return + } + + shared.RespondEphemeral(s, i, "Created project **"+name+"** with ID `"+strconv.FormatInt(id, 10)+"`.") +} + +func modalTextInputValues(components []discordgo.MessageComponent) map[string]string { + values := make(map[string]string) + for _, c := range components { + row, ok := c.(*discordgo.ActionsRow) + if !ok { + continue + } + for _, rc := range row.Components { + ti, ok := rc.(*discordgo.TextInput) + if !ok { + continue + } + values[ti.CustomID] = ti.Value + } + } + return values +} diff --git a/internal/commands/projects/public/projects.go b/internal/commands/projects/public/projects.go index cc2bc43..a4de2a3 100644 --- a/internal/commands/projects/public/projects.go +++ b/internal/commands/projects/public/projects.go @@ -24,39 +24,16 @@ var Projects = &discordgo.ApplicationCommand{ Type: discordgo.ApplicationCommandOptionSubCommand, Name: "create", Description: "Create a new active project", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionString, - Name: "name", - Description: "Project name", - Required: true, - }, - { - Type: discordgo.ApplicationCommandOptionString, - Name: "description", - Description: "Short description (optional)", - Required: false, - }, - }, }, { Type: discordgo.ApplicationCommandOptionSubCommand, Name: "add-helper", Description: "Add a helper to an existing project", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionInteger, - Name: "project-id", - Description: "ID of the project", - Required: true, - }, - { - Type: discordgo.ApplicationCommandOptionUser, - Name: "user", - Description: "User to add as helper", - Required: true, - }, - }, + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "close", + Description: "Close (deactivate) a project", }, }, } @@ -79,6 +56,8 @@ func ProjectsHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { handleCreate(s, i) case "add-helper": handleAddHelper(s, i) + case "close": + handleClose(s, i) default: shared.RespondEphemeral(s, i, "Unknown subcommand.") } @@ -154,56 +133,53 @@ func mentionUser(id int64) string { return "<@" + strconv.FormatInt(id, 10) + ">" } +// handleCreate just opens the modal - the actual creation happens on +// submit, in HandleModalSubmit (components.go). func handleCreate(s *discordgo.Session, i *discordgo.InteractionCreate) { if !shared.RequireManageGuild(s, i) { return } - guildID, ok := shared.ParseGuildID(s, i) - if !ok { - return - } - if i.Member == nil || i.Member.User == nil { - shared.RespondEphemeral(s, i, "Missing member info.") - return - } - - data := i.ApplicationCommandData() - if len(data.Options) == 0 { - shared.RespondEphemeral(s, i, "Missing options.") - return - } - opt := data.Options[0] - - var name, description string - for _, o := range opt.Options { - switch o.Name { - case "name": - name = o.StringValue() - case "description": - description = o.StringValue() - } - } - if strings.TrimSpace(name) == "" { - shared.RespondEphemeral(s, i, "Project name cannot be empty.") - return - } - - creatorID, err := strconv.ParseInt(i.Member.User.ID, 10, 64) - if err != nil { - shared.RespondEphemeral(s, i, "Invalid user ID.") - return - } - - id, err := services.Global.Projects.CreateProject(context.Background(), guildID, creatorID, name, description) - if err != nil { - shared.RespondEphemeral(s, i, "Failed to create project.") - return - } - - shared.RespondEphemeral(s, i, "Created project **"+name+"** with ID `"+strconv.FormatInt(id, 10)+"`.") + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseModal, + Data: &discordgo.InteractionResponseData{ + CustomID: "projects:create_modal", + Title: "Create a project", + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.TextInput{ + CustomID: "name", + Label: "Project name", + Style: discordgo.TextInputShort, + Required: true, + }, + }, + }, + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.TextInput{ + CustomID: "description", + Label: "Description (optional)", + Style: discordgo.TextInputParagraph, + Required: false, + }, + }, + }, + }, + }, + }) } +// handleAddHelper collects the user via Discord's native user-option (a +// modal can't hold one of these), then responds with a select menu of the +// guild's active projects instead of making the caller remember/type a +// numeric project ID - the actual add-helper call happens on selection, in +// HandleComponent (components.go). +// handleAddHelper is the first of two dropdown steps: pick a project here, +// then HandleComponent (components.go) follows up with a native Discord +// user-select to pick who helps, and performs the actual AddHelper call +// once both are known. func handleAddHelper(s *discordgo.Session, i *discordgo.InteractionCreate) { if !shared.RequireManageGuild(s, i) { return @@ -214,55 +190,110 @@ func handleAddHelper(s *discordgo.Session, i *discordgo.InteractionCreate) { return } - data := i.ApplicationCommandData() - if len(data.Options) == 0 { - shared.RespondEphemeral(s, i, "Missing options.") - return - } - opt := data.Options[0] - - var projectID int64 - var userID string - for _, o := range opt.Options { - switch o.Name { - case "project-id": - projectID = o.IntValue() - case "user": - if u := o.UserValue(s); u != nil { - userID = u.ID - } - } - } - if projectID <= 0 { - shared.RespondEphemeral(s, i, "Invalid project ID.") - return - } - if userID == "" { - shared.RespondEphemeral(s, i, "Invalid user.") - return - } - - exists, err := services.Global.Projects.ProjectExistsInGuild(context.Background(), guildID, projectID) + options, err := activeProjectOptions(context.Background(), guildID) if err != nil { - shared.RespondEphemeral(s, i, "Failed to load project.") + shared.RespondEphemeral(s, i, "Failed to load projects.") return } - if !exists { - shared.RespondEphemeral(s, i, "Project `"+strconv.FormatInt(projectID, 10)+"` not found in this server.") + if len(options) == 0 { + shared.RespondEphemeral(s, i, "There are no active projects to add a helper to.") return } - userID64, err := strconv.ParseInt(userID, 10, 64) - if err != nil { - shared.RespondEphemeral(s, i, "Invalid user ID.") - return - } - - if err := services.Global.Projects.AddHelper(context.Background(), projectID, userID64); err != nil { - shared.RespondEphemeral(s, i, "Failed to add helper to project. ("+err.Error()+")") - return - } - - shared.RespondEphemeral(s, i, "Added <@"+userID+"> as helper to project `"+strconv.FormatInt(projectID, 10)+"`.") + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Which project should get a new helper?", + Flags: discordgo.MessageFlagsEphemeral, + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.SelectMenu{ + CustomID: "projects:add_helper_project_select", + Placeholder: "Select a project", + Options: options, + }, + }, + }, + }, + }, + }) } +// handleClose responds with a select menu of the guild's active projects - +// same reasoning as handleAddHelper, picking from a live list beats typing +// an ID. The actual deactivation happens on selection, in HandleComponent. +func handleClose(s *discordgo.Session, i *discordgo.InteractionCreate) { + if !shared.RequireManageGuild(s, i) { + return + } + + guildID, ok := shared.ParseGuildID(s, i) + if !ok { + return + } + + options, err := activeProjectOptions(context.Background(), guildID) + if err != nil { + shared.RespondEphemeral(s, i, "Failed to load projects.") + return + } + if len(options) == 0 { + shared.RespondEphemeral(s, i, "There are no active projects to close.") + return + } + + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Which project should be closed?", + Flags: discordgo.MessageFlagsEphemeral, + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.SelectMenu{ + CustomID: "projects:close_select", + Placeholder: "Select a project", + Options: options, + }, + }, + }, + }, + }, + }) +} + +// activeProjectOptions builds select-menu options for the guild's active +// projects, truncating name/description to Discord's option limits +// (label: 100 chars, description: 100 chars). +func activeProjectOptions(ctx context.Context, guildID int64) ([]discordgo.SelectMenuOption, error) { + const limit = 25 // Discord's own cap on select menu options + items, err := services.Global.Projects.ListActive(ctx, guildID, limit) + if err != nil { + return nil, err + } + + options := make([]discordgo.SelectMenuOption, 0, len(items)) + for _, p := range items { + if p == nil { + continue + } + name := p.Name + if name == "" { + name = "Unnamed project" + } + options = append(options, discordgo.SelectMenuOption{ + Label: truncate(name, 100), + Value: strconv.FormatInt(p.ID, 10), + Description: truncate(p.Description, 100), + }) + } + return options, nil +} + +func truncate(s string, max int) string { + if len(s) <= max { + return s + } + return s[:max] +} diff --git a/internal/commands/projects/registry.go b/internal/commands/projects/registry.go index ff3f81f..a2915f0 100644 --- a/internal/commands/projects/registry.go +++ b/internal/commands/projects/registry.go @@ -12,3 +12,9 @@ var ( func ProjectsHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { public.ProjectsHandler(s, i) } +func HandleComponent(s *discordgo.Session, i *discordgo.InteractionCreate) { public.HandleComponent(s, i) } + +func HandleModalSubmit(s *discordgo.Session, i *discordgo.InteractionCreate) { + public.HandleModalSubmit(s, i) +} + diff --git a/internal/db/repos/projectsrepo/repo.go b/internal/db/repos/projectsrepo/repo.go index 54383d2..aa786b1 100644 --- a/internal/db/repos/projectsrepo/repo.go +++ b/internal/db/repos/projectsrepo/repo.go @@ -61,6 +61,35 @@ func (r *Repo) AddHelper(ctx context.Context, projectID, userID int64) error { return err } +func (r *Repo) DeactivateProject(ctx context.Context, guildID, projectID int64) error { + const q = ` + UPDATE projects + SET is_active = FALSE + WHERE id = $1 AND guild_id = $2 + ` + _, err := r.db.ExecContext(ctx, q, projectID, guildID) + return err +} + +// GetProject fetches a single project, scoped to guildID so a project ID +// from one server can't be used to reach into another's data. +func (r *Repo) GetProject(ctx context.Context, guildID, projectID int64) (*Project, error) { + const q = ` + SELECT id, name, description, created_by + FROM projects + WHERE id = $1 AND guild_id = $2 + ` + row := r.db.QueryRowContext(ctx, q, projectID, guildID) + p := &Project{GuildID: guildID, IsActive: true} + if err := row.Scan(&p.ID, &p.Name, &p.Description, &p.CreatedBy); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + return p, nil +} + func (r *Repo) ProjectExistsInGuild(ctx context.Context, guildID, projectID int64) (bool, error) { const q = ` SELECT 1 @@ -79,6 +108,37 @@ func (r *Repo) ProjectExistsInGuild(ctx context.Context, guildID, projectID int6 } } +// ListActiveProjects is a lighter version of ListActiveProjectsWithMembers, +// without the member-join query - for callers (like building a select menu +// of projects to pick from) that only need name/id, not creator/helpers. +func (r *Repo) ListActiveProjects(ctx context.Context, guildID int64, limit int) ([]*Project, error) { + const q = ` + SELECT id, name, description, created_by + FROM projects + WHERE guild_id = $1 AND is_active = TRUE + ORDER BY created_at DESC + LIMIT $2 + ` + rows, err := r.db.QueryContext(ctx, q, guildID, limit) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []*Project + for rows.Next() { + p := &Project{GuildID: guildID, IsActive: true} + if err := rows.Scan(&p.ID, &p.Name, &p.Description, &p.CreatedBy); err != nil { + return nil, err + } + out = append(out, p) + } + if err := rows.Err(); err != nil { + return nil, err + } + return out, nil +} + func (r *Repo) ListActiveProjectsWithMembers(ctx context.Context, guildID int64, limit int) ([]*ProjectWithMembers, error) { const qProjects = ` SELECT id, name, description, created_by diff --git a/internal/db/repos/twitchrepo/repo.go b/internal/db/repos/twitchrepo/repo.go index f68e441..835064d 100644 --- a/internal/db/repos/twitchrepo/repo.go +++ b/internal/db/repos/twitchrepo/repo.go @@ -113,3 +113,31 @@ func (r *Repo) SetNotificationChannel(ctx context.Context, guildID, channelID in return err } +// IsEnabled reports whether Twitch notifications are enabled for guildID. +// A guild with no twitch_config row yet defaults to enabled, matching the +// column's own DEFAULT TRUE - "never configured" and "explicitly on" are +// the same state until someone actually flips it off. +func (r *Repo) IsEnabled(ctx context.Context, guildID int64) (bool, error) { + const q = `SELECT enabled FROM twitch_config WHERE guild_id = $1` + var enabled bool + err := r.db.QueryRowContext(ctx, q, guildID).Scan(&enabled) + if err == sql.ErrNoRows { + return true, nil + } + if err != nil { + return false, err + } + return enabled, nil +} + +func (r *Repo) SetEnabled(ctx context.Context, guildID int64, enabled bool) error { + const q = ` + INSERT INTO twitch_config (guild_id, enabled) + VALUES ($1, $2) + ON CONFLICT (guild_id) + DO UPDATE SET enabled = EXCLUDED.enabled + ` + _, err := r.db.ExecContext(ctx, q, guildID, enabled) + return err +} + diff --git a/internal/db/services/projects/service.go b/internal/db/services/projects/service.go index e1687c8..09ade7e 100644 --- a/internal/db/services/projects/service.go +++ b/internal/db/services/projects/service.go @@ -26,7 +26,19 @@ func (s *Service) ListActiveWithMembers(ctx context.Context, guildID int64, limi return s.repo.ListActiveProjectsWithMembers(ctx, guildID, limit) } +func (s *Service) ListActive(ctx context.Context, guildID int64, limit int) ([]*projectsrepo.Project, error) { + return s.repo.ListActiveProjects(ctx, guildID, limit) +} + func (s *Service) ProjectExistsInGuild(ctx context.Context, guildID, projectID int64) (bool, error) { return s.repo.ProjectExistsInGuild(ctx, guildID, projectID) } +func (s *Service) GetProject(ctx context.Context, guildID, projectID int64) (*projectsrepo.Project, error) { + return s.repo.GetProject(ctx, guildID, projectID) +} + +func (s *Service) DeactivateProject(ctx context.Context, guildID, projectID int64) error { + return s.repo.DeactivateProject(ctx, guildID, projectID) +} + diff --git a/internal/db/services/twitch/service.go b/internal/db/services/twitch/service.go index 8a2024c..f612c86 100644 --- a/internal/db/services/twitch/service.go +++ b/internal/db/services/twitch/service.go @@ -59,6 +59,14 @@ func (s *Service) SetNotificationChannel(ctx context.Context, guildID, channelID return s.repo.SetNotificationChannel(ctx, guildID, channelID) } +func (s *Service) IsEnabled(ctx context.Context, guildID int64) (bool, error) { + return s.repo.IsEnabled(ctx, guildID) +} + +func (s *Service) SetEnabled(ctx context.Context, guildID int64, enabled bool) error { + return s.repo.SetEnabled(ctx, guildID, enabled) +} + func (s *Service) IsUserStreaming(ctx context.Context, username string) (bool, error) { type gqlRequest struct { Query string `json:"query"` diff --git a/internal/events/twitch_live.go b/internal/events/twitch_live.go index 9f3ae7a..38af8ea 100644 --- a/internal/events/twitch_live.go +++ b/internal/events/twitch_live.go @@ -47,6 +47,16 @@ func StartTwitchLiveLoop(s *discordgo.Session, svc *services.Services) { for _, guildID := range guildIDs { log.Printf("twitch: processing guild %d", guildID) + enabled, err := svc.Twitch.IsEnabled(ctx, guildID) + if err != nil { + log.Printf("twitch: failed to check enabled state for guild %d: %v", guildID, err) + continue + } + if !enabled { + log.Printf("twitch: notifications disabled for guild %d", guildID) + continue + } + channelID, ok, err := svc.Twitch.GetNotificationChannel(ctx, guildID) if err != nil { log.Printf("twitch: failed to get channel for guild %d: %v", guildID, err) diff --git a/internal/music/components.go b/internal/music/components.go index 214b1e5..4e1bc51 100644 --- a/internal/music/components.go +++ b/internal/music/components.go @@ -97,7 +97,10 @@ func HandleComponent(s *discordgo.Session, i *discordgo.InteractionCreate) { content = "Done." } if err != nil { - content = "Error: " + err.Error() + // err can carry a raw Lavalink/network failure here (Player.Update), + // not just our own "music manager not initialized" - don't echo + // that detail into the ephemeral reply. + content = "That didn't work. Please try again." } // send ephemeral confirmation diff --git a/schema.sql b/schema.sql index ab1fd12..a970a6d 100644 --- a/schema.sql +++ b/schema.sql @@ -45,7 +45,8 @@ CREATE TABLE IF NOT EXISTS levelup ( CREATE TABLE IF NOT EXISTS twitch_config ( guild_id BIGINT PRIMARY KEY, - twitch_channel_id BIGINT + twitch_channel_id BIGINT, + enabled BOOLEAN NOT NULL DEFAULT TRUE ); CREATE TABLE IF NOT EXISTS rps (