added suggestion command
This commit is contained in:
@@ -22,3 +22,5 @@
|
|||||||
go.work
|
go.work
|
||||||
|
|
||||||
.env
|
.env
|
||||||
|
*.xml
|
||||||
|
data/
|
||||||
|
|||||||
@@ -1,47 +1,13 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"github.com/bwmarrin/discordgo"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
helpers "ion606_bot/Bot/Helpers"
|
helpers "ion606_bot/Bot/Helpers"
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// reactionRoleTarget maps an interaction’s ID to its target message ID
|
|
||||||
var reactionRoleTarget = make(map[string]string)
|
|
||||||
|
|
||||||
// SaveReactionRoleTarget saves the reactionRoleTarget map to a JSON file.
|
|
||||||
func SaveReactionRoleTarget(filename string) error {
|
|
||||||
data, err := json.Marshal(reactionRoleTarget)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal reactionRoleTarget: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.WriteFile(filename, data, 0644); err != nil {
|
|
||||||
return fmt.Errorf("failed to write reactionRoleTarget file: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadReactionRoleTarget loads the reactionRoleTarget map from a JSON file.
|
|
||||||
func LoadReactionRoleTarget(filename string) error {
|
|
||||||
data, err := os.ReadFile(filename)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read reactionRoleTarget file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(data, &reactionRoleTarget); err != nil {
|
|
||||||
return fmt.Errorf("failed to unmarshal reactionRoleTarget: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the manage roles permission into an int64 pointer for command registration.
|
// Convert the manage roles permission into an int64 pointer for command registration.
|
||||||
func manageRolesPerm() *int64 {
|
func manageRolesPerm() *int64 {
|
||||||
fmt.Println("checking perms")
|
fmt.Println("checking perms")
|
||||||
@@ -214,8 +180,6 @@ func HandleReactionRole(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|||||||
if opt := helpers.GetOption(options, "message_id"); opt != nil {
|
if opt := helpers.GetOption(options, "message_id"); opt != nil {
|
||||||
if val := opt.StringValue(); val != "" {
|
if val := opt.StringValue(); val != "" {
|
||||||
targetMsgID = val
|
targetMsgID = val
|
||||||
// Store the target message ID keyed by the user's ID.
|
|
||||||
reactionRoleTarget[i.Member.User.ID] = targetMsgID
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +279,7 @@ func HandleReactionRoleModalSubmit(s *discordgo.Session, i *discordgo.Interactio
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, innerComp := range row.Components {
|
for _, innerComp := range row.Components {
|
||||||
textInput, ok := innerComp.(*discordgo.TextInput)
|
textInput, ok := innerComp.(*discordgo.TextInput)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
helpers "ion606_bot/Bot/Helpers"
|
||||||
|
|
||||||
|
"github.com/bwmarrin/discordgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
var SuggestCommand = &discordgo.ApplicationCommand{
|
||||||
|
Name: "suggest",
|
||||||
|
Description: "Submit a suggestion to the server",
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleSuggest(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
|
userID := i.Member.User.ID
|
||||||
|
|
||||||
|
// Check cooldown from DB
|
||||||
|
lastSubmission, err := helpers.GetLastSubmission(userID)
|
||||||
|
if err != nil {
|
||||||
|
helpers.HandleError(s, i, fmt.Errorf("failed to check cooldown: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
thirtyMins := time.Minute * time.Duration(30)
|
||||||
|
|
||||||
|
if time.Since(lastSubmission) < (thirtyMins) {
|
||||||
|
remaining := time.Until(lastSubmission.Add(thirtyMins)).Round(time.Minute)
|
||||||
|
|
||||||
|
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
|
Data: &discordgo.InteractionResponseData{
|
||||||
|
Content: fmt.Sprintf("⏳ You can submit another suggestion in %d minutes!", int(remaining.Minutes())),
|
||||||
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store new submission time
|
||||||
|
if err := helpers.SetLastSubmission(userID); err != nil {
|
||||||
|
helpers.HandleError(s, i, fmt.Errorf("failed to update cooldown: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create modal with suggestion input
|
||||||
|
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
|
Type: discordgo.InteractionResponseModal,
|
||||||
|
Data: &discordgo.InteractionResponseData{
|
||||||
|
CustomID: "suggestion_modal",
|
||||||
|
Title: "Submit Suggestion",
|
||||||
|
Components: []discordgo.MessageComponent{
|
||||||
|
discordgo.ActionsRow{
|
||||||
|
Components: []discordgo.MessageComponent{
|
||||||
|
discordgo.TextInput{
|
||||||
|
CustomID: "suggestion_content",
|
||||||
|
Label: "Your suggestion",
|
||||||
|
Style: discordgo.TextInputParagraph,
|
||||||
|
Placeholder: "Type your suggestion here...",
|
||||||
|
Required: true,
|
||||||
|
MaxLength: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
helpers.HandleError(s, i, fmt.Errorf("failed to create modal: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleSuggestModal(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
|
// Extract suggestion from modal
|
||||||
|
data := i.ModalSubmitData()
|
||||||
|
content := data.Components[0].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value
|
||||||
|
|
||||||
|
// Get webhook URL from environment
|
||||||
|
webhookURL := os.Getenv("WEBHOOK_URL")
|
||||||
|
if webhookURL == "" {
|
||||||
|
helpers.HandleError(s, i, fmt.Errorf("WEBHOOK_URL environment variable not set"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create payload
|
||||||
|
payload := map[string]string{
|
||||||
|
"content": fmt.Sprintf("**New Suggestion from <@%s> (%s)**\n```\n%s\n```",
|
||||||
|
i.Member.User.ID,
|
||||||
|
i.Member.User.Username,
|
||||||
|
content),
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
helpers.HandleError(s, i, fmt.Errorf("failed to marshal payload: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to webhook
|
||||||
|
resp, err := http.Post(webhookURL, "application/json", strings.NewReader(string(jsonData)))
|
||||||
|
if err != nil {
|
||||||
|
helpers.HandleError(s, i, fmt.Errorf("webhook request failed: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Verify successful response
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
helpers.HandleError(s, i, fmt.Errorf("webhook returned %d: %s", resp.StatusCode, string(body)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send confirmation to user
|
||||||
|
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
|
Data: &discordgo.InteractionResponseData{
|
||||||
|
Content: "✅ Your suggestion has been submitted!",
|
||||||
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
db *sql.DB
|
||||||
|
dbOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitSuggestionDB() (*sql.DB, error) {
|
||||||
|
var err error
|
||||||
|
dbOnce.Do(func() {
|
||||||
|
// Create data directory if it doesn't exist
|
||||||
|
dataDir := "data"
|
||||||
|
if err = os.MkdirAll(dataDir, 0755); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath := filepath.Join(dataDir, "suggestions.db")
|
||||||
|
db, err = sql.Open("sqlite3", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create table if it doesn't exist
|
||||||
|
_, err = db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS suggestion_cooldowns (
|
||||||
|
user_id TEXT PRIMARY KEY,
|
||||||
|
last_submission TIMESTAMP
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
return db, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLastSubmission(userID string) (time.Time, error) {
|
||||||
|
var lastSubmission time.Time
|
||||||
|
err := db.QueryRow(`
|
||||||
|
SELECT last_submission FROM suggestion_cooldowns
|
||||||
|
WHERE user_id = ?
|
||||||
|
`, userID).Scan(&lastSubmission)
|
||||||
|
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return time.Time{}, nil // No record exists
|
||||||
|
}
|
||||||
|
return lastSubmission, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetLastSubmission(userID string) error {
|
||||||
|
_, err := db.Exec(`
|
||||||
|
INSERT OR REPLACE INTO suggestion_cooldowns
|
||||||
|
(user_id, last_submission) VALUES (?, ?)
|
||||||
|
`, userID, time.Now())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseDB() error {
|
||||||
|
if db != nil {
|
||||||
|
return db.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+14
-9
@@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
commands "ion606_bot/Bot/Commands"
|
commands "ion606_bot/Bot/Commands"
|
||||||
|
helpers "ion606_bot/Bot/Helpers"
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
)
|
)
|
||||||
@@ -25,6 +26,7 @@ var commandHandlers = map[string]func(*discordgo.Session, *discordgo.Interaction
|
|||||||
"animalgif": commands.HandleAnimalGif,
|
"animalgif": commands.HandleAnimalGif,
|
||||||
"action": commands.HandleAction,
|
"action": commands.HandleAction,
|
||||||
"reactionrole": commands.HandleReactionRole,
|
"reactionrole": commands.HandleReactionRole,
|
||||||
|
"suggest": commands.HandleSuggest,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run starts the Discord bot session and listens for both message and slash command events.
|
// Run starts the Discord bot session and listens for both message and slash command events.
|
||||||
@@ -35,12 +37,6 @@ func Run() {
|
|||||||
log.Fatal("Error creating Discord session: ", err)
|
log.Fatal("Error creating Discord session: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load reaction role target map from file.
|
|
||||||
err = commands.LoadReactionRoleTarget("reactionRoleTarget.json")
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("No reaction role target config loaded: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// add event handlers for messages and interactions.
|
// add event handlers for messages and interactions.
|
||||||
discord.AddHandler(newMessage)
|
discord.AddHandler(newMessage)
|
||||||
discord.AddHandler(handleInteractionCreate)
|
discord.AddHandler(handleInteractionCreate)
|
||||||
@@ -50,6 +46,11 @@ func Run() {
|
|||||||
log.Fatal("Error opening Discord session: ", err)
|
log.Fatal("Error opening Discord session: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = helpers.InitSuggestionDB()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to initialize suggestion DB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
RegisterCommands(discord, "")
|
RegisterCommands(discord, "")
|
||||||
|
|
||||||
log.Println("Bot running....")
|
log.Println("Bot running....")
|
||||||
@@ -59,10 +60,9 @@ func Run() {
|
|||||||
signal.Notify(c, os.Interrupt)
|
signal.Notify(c, os.Interrupt)
|
||||||
<-c
|
<-c
|
||||||
|
|
||||||
// Save reaction role target map to file on shutdown.
|
err = helpers.CloseDB()
|
||||||
err = commands.SaveReactionRoleTarget("reactionRoleTarget.json")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error saving reaction role target:", err)
|
log.Println("Error closing suggestion DB:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = discord.Close()
|
err = discord.Close()
|
||||||
@@ -85,6 +85,11 @@ func handleInteractionCreate(s *discordgo.Session, i *discordgo.InteractionCreat
|
|||||||
commands.HandleReactionRoleModalSubmit(s, i)
|
commands.HandleReactionRoleModalSubmit(s, i)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if i.ModalSubmitData().CustomID == "suggestion_modal" {
|
||||||
|
commands.HandleSuggestModal(s, i)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then check for message components (buttons).
|
// Then check for message components (buttons).
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ var commandsList = []*discordgo.ApplicationCommand{
|
|||||||
Commands.AnimalGifCommand,
|
Commands.AnimalGifCommand,
|
||||||
Commands.ActionCommand,
|
Commands.ActionCommand,
|
||||||
Commands.ReactionRoleCommand,
|
Commands.ReactionRoleCommand,
|
||||||
|
Commands.SuggestCommand,
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterCommands(s *discordgo.Session, guildID string) {
|
func RegisterCommands(s *discordgo.Session, guildID string) {
|
||||||
|
|||||||
+9
-2
@@ -1,16 +1,23 @@
|
|||||||
FROM golang:1.24-alpine AS builder
|
FROM golang:1.24-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# for C stuff (CGO_ENABLED --> sqlite3)
|
||||||
|
RUN apk add --no-cache build-base
|
||||||
|
|
||||||
# Copy go.mod and go.sum to download dependencies
|
# Copy go.mod and go.sum to download dependencies
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
# Copy the rest of the code and build the bot binary
|
# Copy the rest of the code
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -o bot .
|
RUN CGO_ENABLED=1 GOOS=linux go build -o bot .
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /app/bot .
|
COPY --from=builder /app/bot .
|
||||||
|
|
||||||
|
# DB stuff
|
||||||
|
VOLUME /app/data
|
||||||
|
RUN mkdir -p /app/data && chmod 755 /app/data
|
||||||
|
|
||||||
CMD ["./bot"]
|
CMD ["./bot"]
|
||||||
@@ -2,10 +2,10 @@ build:
|
|||||||
docker build -t ion606-bot .
|
docker build -t ion606-bot .
|
||||||
|
|
||||||
run: build
|
run: build
|
||||||
docker run --rm -d --env-file .env ion606-bot
|
docker run --rm -d --env-file .env -v "$(shell pwd)"/data:/app/data ion606-bot
|
||||||
|
|
||||||
stop:
|
stop:
|
||||||
docker stop $$(docker ps -q --filter ancestor=ion606-bot)
|
docker stop $$(docker ps -q --filter ancestor=ion606-bot)
|
||||||
|
|
||||||
dev: build
|
dev: build
|
||||||
docker run --rm --env-file .env ion606-bot
|
docker run --rm --env-file .env -v "$(shell pwd)"/data:/app/data ion606-bot
|
||||||
@@ -2,6 +2,8 @@ module ion606_bot
|
|||||||
|
|
||||||
go 1.24.1
|
go 1.24.1
|
||||||
|
|
||||||
|
require github.com/mattn/go-sqlite3 v1.14.27
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bwmarrin/discordgo v0.28.1 // indirect
|
github.com/bwmarrin/discordgo v0.28.1 // indirect
|
||||||
github.com/gorilla/websocket v1.4.2 // indirect
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
|||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad h1:qIQkSlF5vAUHxEmTbaqt1hkJ/t6skqEGYiMag343ucI=
|
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad h1:qIQkSlF5vAUHxEmTbaqt1hkJ/t6skqEGYiMag343ucI=
|
||||||
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s=
|
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s=
|
||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
|
||||||
|
|||||||
Reference in New Issue
Block a user