first commit
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
users.json
|
||||
.env
|
||||
data/
|
||||
*.exe
|
||||
70
Readme.md
Normal file
70
Readme.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Thoughtful Discord Bot
|
||||
|
||||
This project is a Discord bot that integrates with your Thoughtful instance, allowing users to create, list, and delete ideas directly from Discord using slash commands and simple text commands.
|
||||
|
||||
## ✨ Features
|
||||
- **Setup**: Configure your Thoughtful instance URL and API key using `/setup`.
|
||||
- **Create Ideas**: Use `.thought Title; Description` in any channel to create a new idea.
|
||||
- **List Ideas**: Use `/list` to view all your ideas.
|
||||
- **Delete Ideas**: Use `/delete` with autocomplete to remove an idea by title.
|
||||
- **Logout**: Use `/logout` to remove your stored credentials.
|
||||
|
||||
## 🚀 Quick Deployment
|
||||
You can deploy this bot easily using Docker Compose. Follow these steps:
|
||||
|
||||
### 1. Clone the Repository
|
||||
```sh
|
||||
git clone https://github.com/yourusername/thoughtful-dcbob.git
|
||||
cd thoughtful-dcbob
|
||||
```
|
||||
|
||||
Or, if updating an existing deployment:
|
||||
```sh
|
||||
git pull
|
||||
```
|
||||
|
||||
### 2. Set Up Environment Variables
|
||||
Create a `.env` file in the project root with your Discord bot token:
|
||||
|
||||
```
|
||||
DISCORD_BOT_TOKEN=your_discord_bot_token_here
|
||||
```
|
||||
|
||||
### 3. Run with Docker Compose
|
||||
A sample `docker-compose.yml` is provided. Start the bot with:
|
||||
|
||||
```sh
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
This will build and run the bot in the background.
|
||||
|
||||
## 🤖 Usage
|
||||
1. **Invite your bot to your Discord server.**
|
||||
2. **Run `/setup`** to configure your Thoughtful instance (URL and API key).
|
||||
3. **Create ideas** by typing `.thought Title; Description` in any channel.
|
||||
4. **List ideas** with `/list`.
|
||||
5. **Delete ideas** with `/delete` (autocomplete helps you find the right idea).
|
||||
6. **Logout** with `/logout`.
|
||||
|
||||
## 🔧 Configuration Storage
|
||||
User configuration (instance URL and API key) is stored in `users.json` in the project directory. This file is managed automatically by the bot.
|
||||
|
||||
## ⚠️ Intents ⚠️
|
||||
The bot requires the following Discord intents:
|
||||
- Message Content Intent
|
||||
|
||||
## 🤔 Troubleshooting
|
||||
- Make sure your Discord bot token is correct and set in the environment.
|
||||
- Ensure your Thoughtful instance is reachable from the bot.
|
||||
- For any issues, check the logs with:
|
||||
```sh
|
||||
docker compose logs
|
||||
```
|
||||
|
||||
## Contributing
|
||||
Pull requests and issues are welcome!
|
||||
|
||||
---
|
||||
|
||||
**Enjoy using Thoughtful Discord Bot!**
|
||||
122
bot.go
Normal file
122
bot.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"thoughtful-dcbob/commands"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create a new Discord session using the provided bot token.
|
||||
token := os.Getenv("DISCORD_BOT_TOKEN")
|
||||
if token == "" {
|
||||
fmt.Println("DISCORD_BOT_TOKEN not set; aborting")
|
||||
return
|
||||
}
|
||||
dg, err := discordgo.New("Bot " + token)
|
||||
if err != nil {
|
||||
fmt.Println("error creating Discord session,", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Register handlers
|
||||
dg.AddHandler(messageCreate)
|
||||
dg.AddHandler(func(s *discordgo.Session, ic *discordgo.InteractionCreate) {
|
||||
commands.HandleInteraction(s, ic)
|
||||
})
|
||||
|
||||
// Just like the ping pong example, we only care about receiving message
|
||||
// events in this example.
|
||||
dg.Identify.Intents = discordgo.IntentsGuildMessages
|
||||
|
||||
// Open a websocket connection to Discord and begin listening.
|
||||
err = dg.Open()
|
||||
if err != nil {
|
||||
fmt.Println("error opening connection,", err)
|
||||
return
|
||||
}
|
||||
|
||||
// After opening, register slash commands.
|
||||
if err := commands.RegisterAll(dg); err != nil {
|
||||
fmt.Println("error registering commands:", err)
|
||||
// continue running; commands may be registered later
|
||||
}
|
||||
|
||||
// Wait here until CTRL-C or other term signal is received.
|
||||
fmt.Println("Bot is now running. Press CTRL-C to exit.")
|
||||
sc := make(chan os.Signal, 1)
|
||||
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
|
||||
<-sc
|
||||
|
||||
// Cleanly close down the Discord session.
|
||||
dg.Close()
|
||||
}
|
||||
|
||||
// This function will be called (due to AddHandler above) every time a new
|
||||
// message is created on any channel that the authenticated bot has access to.
|
||||
//
|
||||
// It is called whenever a message is created but only when it's sent through a
|
||||
// server as we did not request IntentsDirectMessages.
|
||||
func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||
// Ignore all messages created by the bot itself
|
||||
// This isn't required in this specific example but it's a good practice.
|
||||
if m.Author.ID == s.State.User.ID {
|
||||
return
|
||||
}
|
||||
// Non-slash command: .thought Title; Description
|
||||
if strings.HasPrefix(m.Content, ".thought") {
|
||||
cfg, ok, err := commands.GetUserConfig(m.Author.ID)
|
||||
if err != nil {
|
||||
s.ChannelMessageSend(m.ChannelID, "Storage error.")
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
s.ChannelMessageSend(m.ChannelID, "Please run `/setup` first to configure your Thoughtful instance.")
|
||||
return
|
||||
}
|
||||
|
||||
rest := strings.TrimSpace(m.Content[len(".thought"):])
|
||||
if rest == "" {
|
||||
s.ChannelMessageSend(m.ChannelID, "Usage: .thought Title; Description")
|
||||
return
|
||||
}
|
||||
parts := strings.SplitN(rest, ";", 2)
|
||||
title := strings.TrimSpace(parts[0])
|
||||
desc := ""
|
||||
if len(parts) > 1 {
|
||||
desc = strings.TrimSpace(parts[1])
|
||||
}
|
||||
|
||||
payload := map[string]string{"title": title, "description": desc}
|
||||
b, _ := json.Marshal(payload)
|
||||
|
||||
req, _ := http.NewRequest("POST", cfg.InstanceURL+"/api/ideas/create", bytes.NewReader(b))
|
||||
req.Header.Set("API-Authentication", cfg.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
s.ChannelMessageSend(m.ChannelID, "Failed to send to API: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
s.ChannelMessageSend(m.ChannelID, "Thought created successfully!")
|
||||
} else {
|
||||
s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("Failed to create thought. Status: %d", resp.StatusCode))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
71
commands/commands.go
Normal file
71
commands/commands.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
type Cmd struct {
|
||||
Command *discordgo.ApplicationCommand
|
||||
Handler func(s *discordgo.Session, ic *discordgo.InteractionCreate)
|
||||
AutocompleteHandler func(s *discordgo.Session, ic *discordgo.InteractionCreate)
|
||||
}
|
||||
|
||||
var (
|
||||
commandHandlers = map[string]func(s *discordgo.Session, ic *discordgo.InteractionCreate){}
|
||||
autocompleteHandlers = map[string]func(s *discordgo.Session, ic *discordgo.InteractionCreate){}
|
||||
)
|
||||
|
||||
func getAllCommands() []*Cmd {
|
||||
return []*Cmd{
|
||||
newSetupCommand(),
|
||||
newLogoutCommand(),
|
||||
newListCommand(),
|
||||
newDeleteCommand(),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterAll registers the application commands with Discord and stores handlers.
|
||||
func RegisterAll(s *discordgo.Session) error {
|
||||
// Clean up old commands (optional, but good for dev)
|
||||
// existing, _ := s.ApplicationCommands(s.State.User.ID, "")
|
||||
// for _, v := range existing {
|
||||
// s.ApplicationCommandDelete(s.State.User.ID, "", v.ID)
|
||||
// }
|
||||
|
||||
for _, c := range getAllCommands() {
|
||||
_, err := s.ApplicationCommandCreate(s.State.User.ID, "", c.Command)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating command %s: %w", c.Command.Name, err)
|
||||
}
|
||||
commandHandlers[c.Command.Name] = c.Handler
|
||||
if c.AutocompleteHandler != nil {
|
||||
autocompleteHandlers[c.Command.Name] = c.AutocompleteHandler
|
||||
}
|
||||
}
|
||||
log.Println("Commands registered.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleInteraction routes incoming interaction events to the right handler.
|
||||
func HandleInteraction(s *discordgo.Session, ic *discordgo.InteractionCreate) {
|
||||
switch ic.Type {
|
||||
case discordgo.InteractionApplicationCommand:
|
||||
name := ic.ApplicationCommandData().Name
|
||||
if h, ok := commandHandlers[name]; ok {
|
||||
h(s, ic)
|
||||
}
|
||||
case discordgo.InteractionApplicationCommandAutocomplete:
|
||||
name := ic.ApplicationCommandData().Name
|
||||
if h, ok := autocompleteHandlers[name]; ok {
|
||||
h(s, ic)
|
||||
}
|
||||
case discordgo.InteractionModalSubmit:
|
||||
data := ic.ModalSubmitData()
|
||||
if data.CustomID == "setup_modal" {
|
||||
handleSetupSubmit(s, ic)
|
||||
}
|
||||
}
|
||||
}
|
||||
139
commands/delete.go
Normal file
139
commands/delete.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
func newDeleteCommand() *Cmd {
|
||||
return &Cmd{
|
||||
Command: &discordgo.ApplicationCommand{
|
||||
Name: "delete",
|
||||
Description: "Delete a Thoughtful idea",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Name: "idea",
|
||||
Description: "The idea to delete (search by title)",
|
||||
Required: true,
|
||||
Autocomplete: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Handler: handleDelete,
|
||||
AutocompleteHandler: handleDeleteAutocomplete,
|
||||
}
|
||||
}
|
||||
|
||||
func handleDelete(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
|
||||
}
|
||||
|
||||
options := ic.ApplicationCommandData().Options
|
||||
ideaID := options[0].StringValue()
|
||||
|
||||
payload := map[string]string{"id": ideaID}
|
||||
b, _ := json.Marshal(payload)
|
||||
|
||||
req, _ := http.NewRequest("DELETE", cfg.InstanceURL+"/api/ideas/delete", bytes.NewReader(b))
|
||||
req.Header.Set("API-Authentication", cfg.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
respondError(s, ic, "Failed to connect: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 401 {
|
||||
respondError(s, ic, "Unauthorized.")
|
||||
return
|
||||
}
|
||||
|
||||
// Assuming 200 is success
|
||||
if resp.StatusCode == 200 {
|
||||
s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: "Idea deleted successfully.",
|
||||
Flags: discordgo.MessageFlagsEphemeral,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
respondError(s, ic, fmt.Sprintf("Failed to delete. Status: %d", resp.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeleteAutocomplete(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 || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch ideas
|
||||
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
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result ideaListResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
query := ""
|
||||
data := ic.ApplicationCommandData()
|
||||
for _, opt := range data.Options {
|
||||
if opt.Name == "idea" && opt.Focused {
|
||||
query = strings.ToLower(opt.StringValue())
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
choices := []*discordgo.ApplicationCommandOptionChoice{}
|
||||
for _, idea := range result.Ideas {
|
||||
if query == "" || strings.Contains(strings.ToLower(idea.Title), query) {
|
||||
choices = append(choices, &discordgo.ApplicationCommandOptionChoice{
|
||||
Name: idea.Title,
|
||||
Value: idea.ID,
|
||||
})
|
||||
if len(choices) >= 25 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionApplicationCommandAutocompleteResult,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Choices: choices,
|
||||
},
|
||||
})
|
||||
}
|
||||
115
commands/list.go
Normal file
115
commands/list.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
func newListCommand() *Cmd {
|
||||
return &Cmd{
|
||||
Command: &discordgo.ApplicationCommand{
|
||||
Name: "list",
|
||||
Description: "List your Thoughtful ideas",
|
||||
},
|
||||
Handler: handleList,
|
||||
}
|
||||
}
|
||||
|
||||
type ideaListResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Ideas []Idea `json:"ideas"`
|
||||
}
|
||||
|
||||
type Idea struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func handleList(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
|
||||
}
|
||||
|
||||
// Format output (simple list for now)
|
||||
var content = "**Your Ideas:**\n"
|
||||
for _, idea := range result.Ideas {
|
||||
content += fmt.Sprintf("• **%s** (ID: `%s`)\n", idea.Title, idea.ID)
|
||||
}
|
||||
|
||||
// Discord message limit check (simplified)
|
||||
if len(content) > 2000 {
|
||||
content = content[:1997] + "..."
|
||||
}
|
||||
|
||||
s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: content,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func respondError(s *discordgo.Session, ic *discordgo.InteractionCreate, msg string) {
|
||||
s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: msg,
|
||||
Flags: discordgo.MessageFlagsEphemeral,
|
||||
},
|
||||
})
|
||||
}
|
||||
35
commands/logout.go
Normal file
35
commands/logout.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
func newLogoutCommand() *Cmd {
|
||||
return &Cmd{
|
||||
Command: &discordgo.ApplicationCommand{
|
||||
Name: "logout",
|
||||
Description: "Remove your stored Thoughtful credentials",
|
||||
},
|
||||
Handler: handleLogout,
|
||||
}
|
||||
}
|
||||
|
||||
func handleLogout(s *discordgo.Session, ic *discordgo.InteractionCreate) {
|
||||
user := ic.User
|
||||
if user == nil {
|
||||
user = ic.Member.User
|
||||
}
|
||||
err := DeleteUserConfig(user.ID)
|
||||
content := "Your credentials have been removed."
|
||||
if err != nil {
|
||||
content = "Error removing credentials: " + err.Error()
|
||||
}
|
||||
|
||||
s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: content,
|
||||
Flags: discordgo.MessageFlagsEphemeral,
|
||||
},
|
||||
})
|
||||
}
|
||||
112
commands/setup.go
Normal file
112
commands/setup.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
func newSetupCommand() *Cmd {
|
||||
return &Cmd{
|
||||
Command: &discordgo.ApplicationCommand{
|
||||
Name: "setup",
|
||||
Description: "Configure your Thoughtful instance",
|
||||
},
|
||||
Handler: handleSetup,
|
||||
}
|
||||
}
|
||||
|
||||
func handleSetup(s *discordgo.Session, ic *discordgo.InteractionCreate) {
|
||||
err := s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseModal,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
CustomID: "setup_modal",
|
||||
Title: "Thoughtful Setup",
|
||||
Components: []discordgo.MessageComponent{
|
||||
discordgo.ActionsRow{
|
||||
Components: []discordgo.MessageComponent{
|
||||
discordgo.TextInput{
|
||||
CustomID: "instance_url",
|
||||
Label: "Instance URL",
|
||||
Style: discordgo.TextInputShort,
|
||||
Placeholder: "https://your-thoughtful-instance.com",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
discordgo.ActionsRow{
|
||||
Components: []discordgo.MessageComponent{
|
||||
discordgo.TextInput{
|
||||
CustomID: "api_key",
|
||||
Label: "API Key",
|
||||
Style: discordgo.TextInputShort,
|
||||
Placeholder: "Your API Key",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
// fallback if modal fails (rare)
|
||||
}
|
||||
}
|
||||
|
||||
func handleSetupSubmit(s *discordgo.Session, ic *discordgo.InteractionCreate) {
|
||||
data := ic.ModalSubmitData()
|
||||
url := ""
|
||||
key := ""
|
||||
|
||||
for _, comp := range data.Components {
|
||||
// ActionsRow -> TextInput
|
||||
if row, ok := comp.(*discordgo.ActionsRow); ok {
|
||||
for _, c := range row.Components {
|
||||
if input, ok := c.(*discordgo.TextInput); ok {
|
||||
if input.CustomID == "instance_url" {
|
||||
url = strings.TrimSpace(input.Value)
|
||||
} else if input.CustomID == "api_key" {
|
||||
key = strings.TrimSpace(input.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize URL
|
||||
url = strings.TrimSuffix(url, "/")
|
||||
|
||||
if url == "" || key == "" {
|
||||
s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: "Both URL and API Key are required.",
|
||||
Flags: discordgo.MessageFlagsEphemeral,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
user := ic.User
|
||||
if user == nil {
|
||||
user = ic.Member.User
|
||||
}
|
||||
|
||||
err := SaveUserConfig(user.ID, UserConfig{
|
||||
InstanceURL: url,
|
||||
APIKey: key,
|
||||
})
|
||||
|
||||
content := "Configuration saved successfully!"
|
||||
if err != nil {
|
||||
content = "Error saving configuration: " + err.Error()
|
||||
}
|
||||
|
||||
s.InteractionRespond(ic.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: content,
|
||||
Flags: discordgo.MessageFlagsEphemeral,
|
||||
},
|
||||
})
|
||||
}
|
||||
84
commands/storage.go
Normal file
84
commands/storage.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type UserConfig struct {
|
||||
InstanceURL string `json:"instance_url"`
|
||||
APIKey string `json:"api_key"`
|
||||
}
|
||||
|
||||
var (
|
||||
storePath = filepath.Join("users.json")
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
func ensureDir() error {
|
||||
dir := filepath.Dir(storePath)
|
||||
return os.MkdirAll(dir, 0o755)
|
||||
}
|
||||
|
||||
func GetUserConfig(userID string) (UserConfig, bool, error) {
|
||||
m, err := loadAll()
|
||||
if err != nil {
|
||||
return UserConfig{}, false, err
|
||||
}
|
||||
cfg, ok := m[userID]
|
||||
return cfg, ok, nil
|
||||
}
|
||||
|
||||
func SaveUserConfig(userID string, cfg UserConfig) error {
|
||||
m, err := loadAll()
|
||||
if err != nil {
|
||||
// if load fails, maybe just start fresh?
|
||||
m = make(map[string]UserConfig)
|
||||
}
|
||||
m[userID] = cfg
|
||||
return saveAll(m)
|
||||
}
|
||||
|
||||
func DeleteUserConfig(userID string) error {
|
||||
m, err := loadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delete(m, userID)
|
||||
return saveAll(m)
|
||||
}
|
||||
|
||||
func loadAll() (map[string]UserConfig, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if err := ensureDir(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := os.Stat(storePath); os.IsNotExist(err) {
|
||||
return map[string]UserConfig{}, nil
|
||||
}
|
||||
b, err := os.ReadFile(storePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m map[string]UserConfig
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return map[string]UserConfig{}, nil // return empty on corrupt
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func saveAll(m map[string]UserConfig) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if err := ensureDir(); err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(storePath, b, 0o644)
|
||||
}
|
||||
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
services:
|
||||
bot:
|
||||
image: golang:1.25
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ./:/app
|
||||
command: if [ ! -f .env ]; then echo ".env file not found! Please create one based on .env.example and set your DISCORD_BOT_TOKEN."; exit 1; fi && go mod tidy && go run .
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
12
go.mod
Normal file
12
go.mod
Normal file
@@ -0,0 +1,12 @@
|
||||
module thoughtful-dcbob
|
||||
|
||||
go 1.20
|
||||
|
||||
require github.com/bwmarrin/discordgo v0.29.0
|
||||
|
||||
require (
|
||||
github.com/JexSrs/go-ollama v1.1.1 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
|
||||
)
|
||||
14
go.sum
Normal file
14
go.sum
Normal file
@@ -0,0 +1,14 @@
|
||||
github.com/JexSrs/go-ollama v1.1.1 h1:+dggIeIxkVUgLq2k6l3bJ+5dtHr2+NTacYIz/bjshzE=
|
||||
github.com/JexSrs/go-ollama v1.1.1/go.mod h1:OjK/woUpcpAszwEABTcjwOOy2Sf/q+BYLvU7oAPKVVs=
|
||||
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
|
||||
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
Reference in New Issue
Block a user