feat: ideashow command with interactive controls and todo management

This commit is contained in:
Space-Banane
2026-01-23 13:40:02 +01:00
parent 151c250e62
commit 99281d209a
4 changed files with 978 additions and 4 deletions

2
bot.go
View File

@@ -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()

View File

@@ -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)
}
}

939
commands/ideashow.go Normal file
View File

@@ -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_<ideaID>
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_<ideaID>_<todoIndex>
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_<idea_id> (optionally _<todoIndex>)
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,
},
})
}

View File

@@ -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) {