Compare commits
2 Commits
c20855ae6e
...
99281d209a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99281d209a | ||
|
|
151c250e62 |
102
.github/copilot-instructions.md
vendored
Normal file
102
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Thoughtful Discord Bot - AI Coding Instructions
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
A Go Discord bot that integrates with the Thoughtful API for managing ideas through Discord commands. Built with `discordgo` library, uses JSON for user config persistence.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Component Structure
|
||||||
|
- **bot.go**: Main entry point. Handles Discord session, registers handlers, manages bot lifecycle
|
||||||
|
- **commands/**: Modular command system where each command is a separate file
|
||||||
|
- `commands.go`: Command registry and interaction router
|
||||||
|
- `storage.go`: JSON-based user config persistence (users.json)
|
||||||
|
- Individual command files: `setup.go`, `list.go`, `delete.go`, `logout.go`
|
||||||
|
|
||||||
|
### Key Design Patterns
|
||||||
|
|
||||||
|
**Command Registration Pattern**: Each command implements the `Cmd` struct:
|
||||||
|
```go
|
||||||
|
type Cmd struct {
|
||||||
|
Command *discordgo.ApplicationCommand
|
||||||
|
Handler func(s *discordgo.Session, ic *discordgo.InteractionCreate)
|
||||||
|
AutocompleteHandler func(s *discordgo.Session, ic *discordgo.InteractionCreate) // optional
|
||||||
|
}
|
||||||
|
```
|
||||||
|
All commands are registered via `getAllCommands()` in [commands/commands.go](../commands/commands.go).
|
||||||
|
|
||||||
|
**Dual Command Interface**: The bot supports both:
|
||||||
|
- Slash commands (e.g., `/setup`, `/list`) via Discord's ApplicationCommand API
|
||||||
|
- Legacy text commands (e.g., `.thought Title; Description`) for backward compatibility
|
||||||
|
|
||||||
|
**User State Management**: Per-user configs (Thoughtful instance URL + API key) stored in `users.json`. Thread-safe access via mutex in [commands/storage.go](../commands/storage.go).
|
||||||
|
|
||||||
|
**Modal-Based Setup**: `/setup` uses Discord modals (not option prompts) to collect sensitive credentials privately.
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **Thoughtful API**: External idea management service. Requires:
|
||||||
|
- Base URL (user-provided, e.g., `https://instance.com`)
|
||||||
|
- API key authentication via `API-Authentication` header
|
||||||
|
- Endpoints: `/api/ideas/create`, `/api/ideas/list`, `/api/ideas/delete`
|
||||||
|
- **Discord API**: via `github.com/bwmarrin/discordgo` library
|
||||||
|
|
||||||
|
## Developer Workflows
|
||||||
|
|
||||||
|
### Running Locally
|
||||||
|
```sh
|
||||||
|
# Set environment variable
|
||||||
|
$env:DISCORD_BOT_TOKEN="your_token_here"
|
||||||
|
|
||||||
|
# Run directly
|
||||||
|
go run .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Deployment
|
||||||
|
The bot is designed for Docker Compose deployment. See [docker-compose.yml](../docker-compose.yml):
|
||||||
|
- Uses `golang:1.25` image (note: uncommon version - likely should be `1.21` or `1.22`)
|
||||||
|
- Auto-pulls git changes on container start
|
||||||
|
- Requires `.env` file with `DISCORD_BOT_TOKEN`
|
||||||
|
|
||||||
|
### Adding New Commands
|
||||||
|
1. Create new file in `commands/` (e.g., `mycommand.go`)
|
||||||
|
2. Implement `newMyCommand()` returning `*Cmd`
|
||||||
|
3. Add to `getAllCommands()` in [commands/commands.go](../commands/commands.go)
|
||||||
|
4. Command automatically registers on bot startup
|
||||||
|
|
||||||
|
## Project-Specific Conventions
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Use `respondError()` helper for interaction failures (defined in command files)
|
||||||
|
- Silent failures acceptable for autocomplete handlers
|
||||||
|
- Channel messages for `.thought` command errors
|
||||||
|
|
||||||
|
### User Identification
|
||||||
|
Discord interactions may have `User` OR `Member.User` depending on context:
|
||||||
|
```go
|
||||||
|
user := ic.User
|
||||||
|
if user == nil {
|
||||||
|
user = ic.Member.User
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Always check both when accessing user data.
|
||||||
|
|
||||||
|
### API Communication
|
||||||
|
- Always trim trailing slashes from instance URLs: `url = strings.TrimSuffix(url, "/")`
|
||||||
|
- Use `API-Authentication` header (not `Authorization`)
|
||||||
|
- 401 status codes mean invalid/expired API keys
|
||||||
|
|
||||||
|
### Storage Layer
|
||||||
|
- **Never** edit `users.json` directly - always use `storage.go` functions
|
||||||
|
- Operations are mutex-protected but not transactional
|
||||||
|
- Corrupted JSON automatically resets to empty map
|
||||||
|
|
||||||
|
## Critical Gotchas
|
||||||
|
|
||||||
|
1. **Intents Required**: Bot needs `IntentsGuildMessages` (set in [bot.go](../bot.go#L40)). Message Content Intent must be enabled in Discord Developer Portal.
|
||||||
|
|
||||||
|
2. **Slash Command Updates**: Slash commands persist in Discord. To update, uncomment command cleanup code in [commands/commands.go](../commands/commands.go#L30-L33).
|
||||||
|
|
||||||
|
3. **Modal Response Type**: Setup uses `InteractionResponseModal`, not `InteractionResponseChannelMessageWithSource`.
|
||||||
|
|
||||||
|
4. **Autocomplete Filtering**: Delete command autocomplete should filter by user input but currently returns all ideas - see [commands/delete.go](../commands/delete.go#L86-L100).
|
||||||
|
|
||||||
|
5. **No Database**: All state in `users.json`. Loss of this file = all users must re-run `/setup`.
|
||||||
2
bot.go
2
bot.go
@@ -36,7 +36,7 @@ func main() {
|
|||||||
|
|
||||||
// Just like the ping pong example, we only care about receiving message
|
// Just like the ping pong example, we only care about receiving message
|
||||||
// events in this example.
|
// 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.
|
// Open a websocket connection to Discord and begin listening.
|
||||||
err = dg.Open()
|
err = dg.Open()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package commands
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
)
|
)
|
||||||
@@ -24,6 +25,7 @@ func getAllCommands() []*Cmd {
|
|||||||
newLogoutCommand(),
|
newLogoutCommand(),
|
||||||
newListCommand(),
|
newListCommand(),
|
||||||
newDeleteCommand(),
|
newDeleteCommand(),
|
||||||
|
newIdeashowCommand(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +68,17 @@ func HandleInteraction(s *discordgo.Session, ic *discordgo.InteractionCreate) {
|
|||||||
data := ic.ModalSubmitData()
|
data := ic.ModalSubmitData()
|
||||||
if data.CustomID == "setup_modal" {
|
if data.CustomID == "setup_modal" {
|
||||||
handleSetupSubmit(s, ic)
|
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
939
commands/ideashow.go
Normal 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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -24,9 +24,31 @@ type ideaListResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Idea struct {
|
type Idea struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
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) {
|
func handleList(s *discordgo.Session, ic *discordgo.InteractionCreate) {
|
||||||
|
|||||||
Reference in New Issue
Block a user