From 99281d209a139f42d5b798027297d676e473d8b0 Mon Sep 17 00:00:00 2001 From: Space-Banane Date: Fri, 23 Jan 2026 13:40:02 +0100 Subject: [PATCH] feat: ideashow command with interactive controls and todo management --- bot.go | 2 +- commands/commands.go | 13 + commands/ideashow.go | 939 +++++++++++++++++++++++++++++++++++++++++++ commands/list.go | 28 +- 4 files changed, 978 insertions(+), 4 deletions(-) create mode 100644 commands/ideashow.go diff --git a/bot.go b/bot.go index e2e8136..345b5a7 100644 --- a/bot.go +++ b/bot.go @@ -36,7 +36,7 @@ func main() { // Just like the ping pong example, we only care about receiving message // events in this example. - dg.Identify.Intents = discordgo.IntentsGuildMessages + dg.Identify.Intents = discordgo.IntentsGuildMessages | discordgo.IntentsGuilds | discordgo.IntentsMessageContent // Open a websocket connection to Discord and begin listening. err = dg.Open() diff --git a/commands/commands.go b/commands/commands.go index 40b9268..6ccf97a 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -3,6 +3,7 @@ package commands import ( "fmt" "log" + "strings" "github.com/bwmarrin/discordgo" ) @@ -24,6 +25,7 @@ func getAllCommands() []*Cmd { newLogoutCommand(), newListCommand(), newDeleteCommand(), + newIdeashowCommand(), } } @@ -66,6 +68,17 @@ func HandleInteraction(s *discordgo.Session, ic *discordgo.InteractionCreate) { data := ic.ModalSubmitData() if data.CustomID == "setup_modal" { handleSetupSubmit(s, ic) + } else if strings.HasPrefix(data.CustomID, "ideashow_add_todo_modal_") { + handleAddTodoSubmit(s, ic) } + case discordgo.InteractionMessageComponent: + handleComponent(s, ic) + } +} + +func handleComponent(s *discordgo.Session, ic *discordgo.InteractionCreate) { + data := ic.MessageComponentData() + if strings.HasPrefix(data.CustomID, "ideashow_") { + handleIdeashowComponent(s, ic) } } diff --git a/commands/ideashow.go b/commands/ideashow.go new file mode 100644 index 0000000..3b37c8d --- /dev/null +++ b/commands/ideashow.go @@ -0,0 +1,939 @@ +package commands + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "strconv" + "strings" + + "github.com/bwmarrin/discordgo" +) + +func newIdeashowCommand() *Cmd { + return &Cmd{ + Command: &discordgo.ApplicationCommand{ + Name: "ideashow", + Description: "Show your ideas with interactive controls", + }, + Handler: handleIdeashow, + } +} + +func handleIdeashow(s *discordgo.Session, ic *discordgo.InteractionCreate) { + user := ic.User + if user == nil { + user = ic.Member.User + } + cfg, ok, err := GetUserConfig(user.ID) + if err != nil { + respondError(s, ic, "Error retrieving configuration.") + return + } + if !ok { + respondError(s, ic, "You are not logged in. Use `/setup` first.") + return + } + + client := &http.Client{} + req, _ := http.NewRequest("GET", cfg.InstanceURL+"/api/ideas/list", nil) + req.Header.Set("API-Authentication", cfg.APIKey) + + resp, err := client.Do(req) + if err != nil { + respondError(s, ic, "Failed to connect to Thoughtful instance: "+err.Error()) + return + } + defer resp.Body.Close() + + if resp.StatusCode == 401 { + respondError(s, ic, "Unauthorized. Check your API Key.") + return + } + if resp.StatusCode != 200 { + respondError(s, ic, fmt.Sprintf("API Error: %d", resp.StatusCode)) + return + } + + var result ideaListResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + respondError(s, ic, "Failed to parse API response.") + return + } + + if !result.Success { + respondError(s, ic, "API reported failure.") + return + } + + if len(result.Ideas) == 0 { + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "No ideas found.", + }, + }) + return + } + + // Show first page + page := 0 + embed, components := buildIdeasEmbed(result.Ideas, page) + + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{embed}, + Components: components, + }, + }) +} + +func buildIdeasEmbed(ideas []Idea, page int) (*discordgo.MessageEmbed, []discordgo.MessageComponent) { + perPage := 4 + start := page * perPage + end := start + perPage + if end > len(ideas) { + end = len(ideas) + } + + embed := &discordgo.MessageEmbed{ + Title: "Your Ideas", + Color: 0x00ff00, + Fields: []*discordgo.MessageEmbedField{}, + Footer: &discordgo.MessageEmbedFooter{ + Text: fmt.Sprintf("Page %d", page+1), + }, + } + + for i := start; i < end; i++ { + index := i + 1 + idea := ideas[i] + field := &discordgo.MessageEmbedField{ + Name: fmt.Sprintf("%d. %s", index, idea.Title), + Value: idea.Description, + Inline: false, + } + embed.Fields = append(embed.Fields, field) + } + + // Components + components := []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: "Prev", + Style: discordgo.SecondaryButton, + CustomID: fmt.Sprintf("ideashow_prev_%d", page), + Disabled: page == 0, + }, + discordgo.Button{ + Label: "Next", + Style: discordgo.SecondaryButton, + CustomID: fmt.Sprintf("ideashow_next_%d", page), + Disabled: end >= len(ideas), + }, + }, + }, + } + + if end-start > 0 { + row := discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{}, + } + for i := start; i < end; i++ { + local := i - start + 1 + row.Components = append(row.Components, discordgo.Button{ + Label: strconv.Itoa(local), + Style: discordgo.PrimaryButton, + CustomID: fmt.Sprintf("ideashow_select_%d", i), + }) + } + components = append(components, row) + } + + return embed, components +} + +func buildIdeaEmbed(idea Idea) (*discordgo.MessageEmbed, []discordgo.MessageComponent) { + embed := &discordgo.MessageEmbed{ + Title: idea.Title, + Description: idea.Description, + Color: 0x00ff00, + Fields: []*discordgo.MessageEmbedField{}, + } + + if len(idea.Tags) > 0 { + embed.Fields = append(embed.Fields, &discordgo.MessageEmbedField{ + Name: "Tags", + Value: strings.Join(idea.Tags, ", "), + Inline: true, + }) + } + + if len(idea.Todos) > 0 { + todoText := "" + for _, todo := range idea.Todos { + todoText += fmt.Sprintf("**%s**\n", todo.Title) + for _, item := range todo.Items { + status := "☐" + if item.Completed { + status = "☑" + } + todoText += fmt.Sprintf("%s %s\n", status, item.Text) + } + todoText += "\n" + } + embed.Fields = append(embed.Fields, &discordgo.MessageEmbedField{ + Name: "Todos", + Value: todoText, + Inline: false, + }) + } + + components := []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: "Add Todo", + Style: discordgo.SuccessButton, + CustomID: fmt.Sprintf("ideashow_add_todo_%s", idea.ID), + }, + discordgo.Button{ + Label: "Check Todo", + Style: discordgo.PrimaryButton, + CustomID: fmt.Sprintf("ideashow_check_todo_%s", idea.ID), + }, + }, + }, + } + + return embed, components +} + +func buildToggleEmbed(idea Idea) (*discordgo.MessageEmbed, []discordgo.MessageComponent) { + embed := &discordgo.MessageEmbed{ + Title: "Toggle Todo Items", + Description: fmt.Sprintf("**%s**\n%s", idea.Title, idea.Description), + Color: 0x0000ff, + } + + components := []discordgo.MessageComponent{} + + row := discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{}, + } + + for todoIndex, todo := range idea.Todos { + for itemIndex, item := range todo.Items { + label := item.Text + if len(label) > 80 { + label = label[:77] + "..." + } + style := discordgo.SecondaryButton + if item.Completed { + style = discordgo.SuccessButton + } + row.Components = append(row.Components, discordgo.Button{ + Label: label, + Style: style, + CustomID: fmt.Sprintf("ideashow_toggle_%s_%d_%d", idea.ID, todoIndex, itemIndex), + }) + if len(row.Components) == 5 { + components = append(components, row) + row = discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{}, + } + } + } + } + + if len(row.Components) > 0 { + components = append(components, row) + } + + // Add back button + components = append(components, discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: "Back", + Style: discordgo.SecondaryButton, + CustomID: fmt.Sprintf("ideashow_back_%s", idea.ID), + }, + }, + }) + + return embed, components +} + +func toggleTodoItem(cfg UserConfig, ideaID string, todoIndex, itemIndex int) (*Idea, error) { + // First, fetch the idea + client := &http.Client{} + req, _ := http.NewRequest("GET", cfg.InstanceURL+"/api/ideas/list", nil) + req.Header.Set("API-Authentication", cfg.APIKey) + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("API error: %d", resp.StatusCode) + } + + var result ideaListResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + var idea *Idea + for i := range result.Ideas { + if result.Ideas[i].ID == ideaID { + idea = &result.Ideas[i] + break + } + } + if idea == nil { + return nil, fmt.Errorf("idea not found") + } + + if todoIndex < 0 || todoIndex >= len(idea.Todos) { + return nil, fmt.Errorf("invalid todo index") + } + todo := &idea.Todos[todoIndex] + if itemIndex < 0 || itemIndex >= len(todo.Items) { + return nil, fmt.Errorf("invalid item index") + } + item := &todo.Items[itemIndex] + item.Completed = !item.Completed + + // Update via PUT + updateURL := cfg.InstanceURL + "/api/ideas/update/" + updatePayload := map[string]interface{}{ + "id": idea.ID, + "todos": idea.Todos, + } + b, _ := json.Marshal(updatePayload) + req, _ = http.NewRequest("PUT", updateURL, bytes.NewReader(b)) + req.Header.Set("API-Authentication", cfg.APIKey) + req.Header.Set("Content-Type", "application/json") + + resp, err = client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + log.Printf("[IDEASHOW] Update failed with status: %d", resp.StatusCode) + return nil, fmt.Errorf("update failed: %d", resp.StatusCode) + } + + return idea, nil +} + +func handleIdeashowComponent(s *discordgo.Session, ic *discordgo.InteractionCreate) { + data := ic.MessageComponentData() + user := ic.User + if user == nil { + user = ic.Member.User + } + + // Refetch ideas + cfg, ok, err := GetUserConfig(user.ID) + if err != nil { + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Error retrieving configuration.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + if !ok { + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "You are not logged in.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + client := &http.Client{} + req, _ := http.NewRequest("GET", cfg.InstanceURL+"/api/ideas/list", nil) + req.Header.Set("API-Authentication", cfg.APIKey) + + resp, err := client.Do(req) + if err != nil { + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Failed to connect.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "API Error.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + var result ideaListResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Parse error.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + if !result.Success || len(result.Ideas) == 0 { + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "No ideas.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + parts := strings.Split(data.CustomID, "_") + if len(parts) < 2 { + return + } + + switch parts[1] { + case "prev": + if len(parts) < 3 { + return + } + page, err := strconv.Atoi(parts[2]) + if err != nil { + return + } + newPage := page - 1 + if newPage < 0 { + return + } + embed, components := buildIdeasEmbed(result.Ideas, newPage) + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{embed}, + Components: components, + }, + }) + case "next": + if len(parts) < 3 { + return + } + page, err := strconv.Atoi(parts[2]) + if err != nil { + return + } + newPage := page + 1 + perPage := 4 + if newPage*perPage >= len(result.Ideas) { + return + } + embed, components := buildIdeasEmbed(result.Ideas, newPage) + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{embed}, + Components: components, + }, + }) + case "select": + if len(parts) < 3 { + return + } + index, err := strconv.Atoi(parts[2]) + if err != nil || index < 0 || index >= len(result.Ideas) { + return + } + idea := result.Ideas[index] + embed, components := buildIdeaEmbed(idea) + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{embed}, + Components: components, + }, + }) + case "add": + if len(parts) < 4 { + return + } + sub := parts[2] + var ideaID string + if sub == "todo" { + ideaID := parts[3] + // find idea + var idea *Idea + for i := range result.Ideas { + if result.Ideas[i].ID == ideaID { + idea = &result.Ideas[i] + break + } + } + if idea == nil { + return + } + // If there are existing todos, ask whether to create new or add to existing + if len(idea.Todos) > 0 { + // Build components: New Todo button + buttons for existing todos (max 5) + rows := []discordgo.MessageComponent{} + rows = append(rows, discordgo.ActionsRow{Components: []discordgo.MessageComponent{ + discordgo.Button{Label: "New Todo", Style: discordgo.PrimaryButton, CustomID: fmt.Sprintf("ideashow_add_new_%s", ideaID)}, + }}) + // Add a row of existing todo buttons + row := discordgo.ActionsRow{Components: []discordgo.MessageComponent{}} + for ti := 0; ti < len(idea.Todos) && ti < 5; ti++ { + label := idea.Todos[ti].Title + if len(label) > 80 { + label = label[:77] + "..." + } + row.Components = append(row.Components, discordgo.Button{Label: label, Style: discordgo.SecondaryButton, CustomID: fmt.Sprintf("ideashow_add_to_%s_%d", ideaID, ti)}) + } + rows = append(rows, row) + // Update the message to show choice + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Content: "Choose to create a new todo list or add to an existing one:", + Components: rows, + }, + }) + return + } + // No existing todos; open modal for new todo + ideaID = parts[3] + modal := &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseModal, + Data: &discordgo.InteractionResponseData{ + CustomID: "ideashow_add_todo_modal_" + ideaID, + Title: "Add Todo", + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.TextInput{ + CustomID: "title", + Label: "Todo Title", + Style: discordgo.TextInputShort, + Placeholder: "Enter todo title", + Required: true, + MaxLength: 200, + }, + }, + }, + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.TextInput{ + CustomID: "items", + Label: "Todo Items (one per line)", + Style: discordgo.TextInputParagraph, + Placeholder: "Item 1\nItem 2\nItem 3", + Required: true, + MaxLength: 4000, + }, + }, + }, + }, + }, + } + err := s.InteractionRespond(ic.Interaction, modal) + if err != nil { + log.Printf("[IDEASHOW] Failed to open modal: %v", err) + return + } + return + } else if sub == "new" { + // pattern: ideashow_add_new_ + if len(parts) < 4 { + return + } + ideaID = parts[3] + modal := &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseModal, + Data: &discordgo.InteractionResponseData{ + CustomID: "ideashow_add_todo_modal_" + ideaID, + Title: "Add Todo", + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.TextInput{CustomID: "title", Label: "Todo Title", Style: discordgo.TextInputShort, Placeholder: "Enter todo title", Required: true, MaxLength: 200}, + }, + }, + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.TextInput{CustomID: "items", Label: "Todo Items (one per line)", Style: discordgo.TextInputParagraph, Placeholder: "Item 1\nItem 2\nItem 3", Required: true, MaxLength: 4000}, + }, + }, + }, + }, + } + err := s.InteractionRespond(ic.Interaction, modal) + if err != nil { + log.Printf("[IDEASHOW] Failed to open modal: %v", err) + return + } + return + } else if sub == "to" { + // pattern: ideashow_add_to__ + if len(parts) < 5 { + return + } + ideaID = parts[3] + todoIndex := parts[4] + modal := &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseModal, + Data: &discordgo.InteractionResponseData{ + CustomID: fmt.Sprintf("ideashow_add_todo_modal_%s_%s", ideaID, todoIndex), + Title: "Add Items to Todo", + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.TextInput{CustomID: "items", Label: "Todo Items (one per line)", Style: discordgo.TextInputParagraph, Placeholder: "Item 1\nItem 2\nItem 3", Required: true, MaxLength: 4000}, + }, + }, + }, + }, + } + err := s.InteractionRespond(ic.Interaction, modal) + if err != nil { + log.Printf("[IDEASHOW] Failed to open modal for existing todo: %v", err) + return + } + return + } + case "check": + if len(parts) < 4 || parts[2] != "todo" { + return + } + ideaID := parts[3] + // Find the idea + var idea *Idea + for i := range result.Ideas { + if result.Ideas[i].ID == ideaID { + idea = &result.Ideas[i] + break + } + } + if idea == nil { + return + } + embed, components := buildToggleEmbed(*idea) + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{embed}, + Components: components, + }, + }) + case "toggle": + if len(parts) < 5 { + return + } + ideaID := parts[2] + todoIndex, err1 := strconv.Atoi(parts[3]) + itemIndex, err2 := strconv.Atoi(parts[4]) + if err1 != nil || err2 != nil { + return + } + // Update the item + idea, err := toggleTodoItem(cfg, ideaID, todoIndex, itemIndex) + if err != nil { + log.Printf("[IDEASHOW] Failed to toggle item: %v", err) + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Failed to update: " + err.Error(), + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + embed, components := buildToggleEmbed(*idea) + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{embed}, + Components: components, + }, + }) + case "back": + if len(parts) < 3 { + return + } + ideaID := parts[2] + var idea *Idea + for i := range result.Ideas { + if result.Ideas[i].ID == ideaID { + idea = &result.Ideas[i] + break + } + } + if idea == nil { + return + } + embed, components := buildIdeaEmbed(*idea) + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{embed}, + Components: components, + }, + }) + } +} + +func handleAddTodoSubmit(s *discordgo.Session, ic *discordgo.InteractionCreate) { + data := ic.ModalSubmitData() + user := ic.User + if user == nil { + user = ic.Member.User + } + + parts := strings.Split(data.CustomID, "_") + if len(parts) < 5 { + return + } + ideaID := parts[4] // ideashow_add_todo_modal_ (optionally _) + targetTodoIndex := -1 + if len(parts) >= 6 { + if idx, err := strconv.Atoi(parts[5]); err == nil { + targetTodoIndex = idx + } + } + + cfg, ok, err := GetUserConfig(user.ID) + if err != nil { + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Config error.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + if !ok { + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Not logged in.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + // Fetch idea + client := &http.Client{} + req, _ := http.NewRequest("GET", cfg.InstanceURL+"/api/ideas/list", nil) + req.Header.Set("API-Authentication", cfg.APIKey) + + resp, err := client.Do(req) + if err != nil { + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Fetch error.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "API error.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + var result ideaListResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Parse error.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + var idea *Idea + for i := range result.Ideas { + if result.Ideas[i].ID == ideaID { + idea = &result.Ideas[i] + break + } + } + if idea == nil { + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Idea not found.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + // Parse modal data + var title, itemsText string + for _, comp := range data.Components { + row := comp.(*discordgo.ActionsRow) + for _, c := range row.Components { + if input, ok := c.(*discordgo.TextInput); ok { + if input.CustomID == "title" { + title = input.Value + } else if input.CustomID == "items" { + itemsText = input.Value + } + } + } + } + + if targetTodoIndex >= 0 { + // Adding items to existing todo: only itemsText required + if itemsText == "" { + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Invalid input.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + } else { + if title == "" || itemsText == "" { + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Invalid input.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + } + + // Parse items + lines := strings.Split(itemsText, "\n") + var items []TodoItem + for _, line := range lines { + line = strings.TrimSpace(line) + if line != "" { + items = append(items, TodoItem{ + ID: fmt.Sprintf("item_%d", len(items)), + Text: line, + Completed: false, + }) + } + } + + if targetTodoIndex >= 0 { + // Append items to existing todo + if targetTodoIndex < 0 || targetTodoIndex >= len(idea.Todos) { + log.Printf("[IDEASHOW] Invalid target todo index: %d", targetTodoIndex) + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Invalid todo selection.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + base := &idea.Todos[targetTodoIndex] + for _, it := range items { + it.ID = fmt.Sprintf("item_%d", len(base.Items)) + base.Items = append(base.Items, it) + } + } else { + // Add todo + newTodo := Todo{ + ID: fmt.Sprintf("todo_%d", len(idea.Todos)), + Title: title, + Items: items, + } + idea.Todos = append(idea.Todos, newTodo) + } + + // Update + updateURL := cfg.InstanceURL + "/api/ideas/update/" + updatePayload := map[string]interface{}{ + "id": idea.ID, + "todos": idea.Todos, + } + b, _ := json.Marshal(updatePayload) + req, _ = http.NewRequest("PUT", updateURL, bytes.NewReader(b)) + req.Header.Set("API-Authentication", cfg.APIKey) + req.Header.Set("Content-Type", "application/json") + + resp, err = client.Do(req) + if err != nil { + log.Printf("[IDEASHOW] Add todo request error: %v", err) + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Update error.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + defer resp.Body.Close() + + + if resp.StatusCode != 200 { + log.Printf("[IDEASHOW] Add todo update failed with status: %d", resp.StatusCode) + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Update failed.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + // Respond with updated embed + embed, components := buildIdeaEmbed(*idea) + s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{embed}, + Components: components, + }, + }) +} diff --git a/commands/list.go b/commands/list.go index da7c6b8..7a6ee14 100644 --- a/commands/list.go +++ b/commands/list.go @@ -24,9 +24,31 @@ type ideaListResponse struct { } type Idea struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Tags []string `json:"tags,omitempty"` + Icon string `json:"icon,omitempty"` + StatusId string `json:"statusId,omitempty"` + Todos []Todo `json:"todos,omitempty"` + Resources []Resource `json:"resources,omitempty"` +} + +type Todo struct { + ID string `json:"id"` + Title string `json:"title"` + Items []TodoItem `json:"items"` +} + +type TodoItem struct { + ID string `json:"id"` + Text string `json:"text"` + Completed bool `json:"completed"` +} + +type Resource struct { + Name string `json:"name"` + Link string `json:"link"` } func handleList(s *discordgo.Session, ic *discordgo.InteractionCreate) {