From b7c04b8d7ebd6c73b23371da431c7f2d813e06f2 Mon Sep 17 00:00:00 2001 From: Vuks69 <51289041+Vuks69@users.noreply.github.com> Date: Fri, 15 May 2026 22:23:52 +0200 Subject: [PATCH 1/3] feat: reimplement command handling system with prefix-based dispatching --- internal/core/cmdHandler.go | 107 ++++++++++++++++++++++++++++++++++++ internal/core/start.go | 43 --------------- 2 files changed, 107 insertions(+), 43 deletions(-) create mode 100644 internal/core/cmdHandler.go diff --git a/internal/core/cmdHandler.go b/internal/core/cmdHandler.go new file mode 100644 index 0000000..d108bd6 --- /dev/null +++ b/internal/core/cmdHandler.go @@ -0,0 +1,107 @@ +package core + +import ( + "strings" + "sync" + + "github.com/disgoorg/disgo/events" +) + +type MessageCommandHandler func(event *events.MessageCreate, args []string) + +type commandNode struct { + handler MessageCommandHandler + children map[string]*commandNode +} + +var ( + rootCommand = &commandNode{children: make(map[string]*commandNode)} + registryMutex sync.RWMutex + botPrefix = "!" // configurable command prefix +) + +// SetBotPrefix updates the command prefix at runtime. +func SetBotPrefix(prefix string) { + botPrefix = prefix +} + +// GetBotPrefix returns the active command prefix. +func GetBotPrefix() string { + return botPrefix +} + +// RegisterCommand registers a top-level command handler. +func RegisterCommand(command string, handler func(*events.MessageCreate)) { + RegisterCommandPath([]string{command}, func(event *events.MessageCreate, args []string) { + handler(event) + }) +} + +// RegisterCommandPath registers a handler for a path of nested command names. +func RegisterCommandPath(path []string, handler MessageCommandHandler) { + if len(path) == 0 || handler == nil { + return + } + + registryMutex.Lock() + defer registryMutex.Unlock() + + node := rootCommand + for _, segment := range path { + if node.children == nil { + node.children = make(map[string]*commandNode) + } + child, exists := node.children[segment] + if !exists { + child = &commandNode{children: make(map[string]*commandNode)} + node.children[segment] = child + } + node = child + } + node.handler = handler +} + +// RegisterSubcommand registers a nested handler under the given parent path. +func RegisterSubcommand(parent []string, command string, handler MessageCommandHandler) { + path := append(append([]string(nil), parent...), command) + RegisterCommandPath(path, handler) +} + +// DispatchCommand dispatches a prefix-based message command to the deepest registered handler. +func DispatchCommand(event *events.MessageCreate) { + if event.Message.Author.Bot { + return + } + content := strings.TrimSpace(event.Message.Content) + if !strings.HasPrefix(content, botPrefix) { + return + } + words := strings.Fields(strings.TrimPrefix(content, botPrefix)) + if len(words) == 0 { + return + } + + registryMutex.RLock() + defer registryMutex.RUnlock() + + node := rootCommand + var lastHandler MessageCommandHandler + var lastMatch int + for i, word := range words { + child, exists := node.children[word] + if !exists { + break + } + node = child + if node.handler != nil { + lastHandler = node.handler + lastMatch = i + 1 + } + } + + if lastHandler != nil { + lastHandler(event, words[lastMatch:]) + } +} + +// Slash commands in Disgo are handled separately via application command registration and interaction event listeners. diff --git a/internal/core/start.go b/internal/core/start.go index e620a3f..fb6fb65 100644 --- a/internal/core/start.go +++ b/internal/core/start.go @@ -5,8 +5,6 @@ import ( "log/slog" "os" "os/signal" - "strings" - "sync" "syscall" "github.com/disgoorg/disgo" @@ -15,47 +13,6 @@ import ( "github.com/disgoorg/disgo/gateway" ) -var ( - commandRegistry = make(map[string]func(*events.MessageCreate)) - registryMutex sync.RWMutex - botPrefix = "!" // configurable command prefix -) - -// SetBotPrefix sets the command prefix (default is "!") -func SetBotPrefix(prefix string) { - botPrefix = prefix -} - -// RegisterCommand registers a command handler -func RegisterCommand(command string, handler func(*events.MessageCreate)) { - registryMutex.Lock() - defer registryMutex.Unlock() - commandRegistry[command] = handler -} - -// DispatchCommand dispatches a message to the appropriate command handler -func DispatchCommand(event *events.MessageCreate) { - if event.Message.Author.Bot { - return - } - content := strings.TrimSpace(event.Message.Content) - if !strings.HasPrefix(content, botPrefix) { - return - } - parts := strings.Fields(content) - if len(parts) == 0 { - return - } - // Remove prefix from command to match registry key - command := strings.TrimPrefix(parts[0], botPrefix) - registryMutex.RLock() - handler, exists := commandRegistry[command] - registryMutex.RUnlock() - if exists { - handler(event) - } -} - func Start(ctx context.Context, token string, listener func(*events.MessageCreate)) error { slog.Info("starting pen bot...") slog.Info("disgo version", slog.String("version", disgo.Version)) From d6644490536849715f436942448df37a786e391f Mon Sep 17 00:00:00 2001 From: Vuks69 <51289041+Vuks69@users.noreply.github.com> Date: Fri, 15 May 2026 22:24:35 +0200 Subject: [PATCH 2/3] feat: add configuration command for setting bot prefix --- cmd/pen-fun/main.go | 3 +++ internal/config/commands.go | 46 +++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 internal/config/commands.go diff --git a/cmd/pen-fun/main.go b/cmd/pen-fun/main.go index e2f1d28..77a6c70 100644 --- a/cmd/pen-fun/main.go +++ b/cmd/pen-fun/main.go @@ -6,12 +6,15 @@ import ( "os" "github.com/Neon-Genesis-Linux/pen-bot/internal/community" + "github.com/Neon-Genesis-Linux/pen-bot/internal/config" "github.com/Neon-Genesis-Linux/pen-bot/internal/core" + _ "github.com/Neon-Genesis-Linux/pen-bot/internal/logger" "github.com/disgoorg/disgo/events" ) func main() { community.Register() + config.Register() if err := core.Start(context.Background(), os.Getenv("BOT_TOKEN"), onMessageCreate); err != nil { slog.Error("failed to start pen-fun bot", slog.Any("error", err)) } diff --git a/internal/config/commands.go b/internal/config/commands.go new file mode 100644 index 0000000..835bac8 --- /dev/null +++ b/internal/config/commands.go @@ -0,0 +1,46 @@ +package config + +import ( + "os" + "strings" + + "github.com/Neon-Genesis-Linux/pen-bot/internal/core" + "github.com/Neon-Genesis-Linux/pen-bot/internal/messaging" + "github.com/disgoorg/disgo/events" +) + +const ownerEnv = "BOT_OWNER_ID" + +// Register registers configuration commands with the bot. +func Register() { + core.RegisterCommandPath([]string{"config", "prefix", "set"}, handlePrefixSet) +} + +func handlePrefixSet(event *events.MessageCreate, args []string) { + if !isOwner(event) { + _ = messaging.SendReply(event, "Unauthorized: only bot owner can run config commands.") + return + } + + if len(args) < 1 { + _ = messaging.SendReply(event, "Usage: "+core.GetBotPrefix()+"config prefix set ") + return + } + + newPrefix := strings.TrimSpace(args[0]) + if newPrefix == "" { + _ = messaging.SendReply(event, "Prefix cannot be empty.") + return + } + + core.SetBotPrefix(newPrefix) + _ = messaging.Send(event, "Command prefix set to `"+newPrefix+"`.") +} + +func isOwner(event *events.MessageCreate) bool { + ownerID := os.Getenv(ownerEnv) + if ownerID == "" { + return false + } + return event.Message.Author.ID.String() == ownerID +} From 65ac0016487f049653de42da409d62dd88baea13 Mon Sep 17 00:00:00 2001 From: Vuks69 <51289041+Vuks69@users.noreply.github.com> Date: Fri, 15 May 2026 22:25:31 +0200 Subject: [PATCH 3/3] feat: create wrapper for commonly used message functions --- internal/community/commands.go | 6 ++-- internal/messaging/message.go | 51 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 internal/messaging/message.go diff --git a/internal/community/commands.go b/internal/community/commands.go index 9087130..a6ed170 100644 --- a/internal/community/commands.go +++ b/internal/community/commands.go @@ -2,7 +2,7 @@ package community import ( "github.com/Neon-Genesis-Linux/pen-bot/internal/core" - "github.com/disgoorg/disgo/discord" + "github.com/Neon-Genesis-Linux/pen-bot/internal/messaging" "github.com/disgoorg/disgo/events" ) @@ -13,9 +13,9 @@ func Register() { } func handlePing(event *events.MessageCreate) { - _, _ = event.Client().Rest.CreateMessage(event.ChannelID, discord.NewMessageCreate().WithContent("pong")) + _ = messaging.SendReply(event, "pong") } func handlePong(event *events.MessageCreate) { - _, _ = event.Client().Rest.CreateMessage(event.ChannelID, discord.NewMessageCreate().WithContent("ping")) + _ = messaging.SendReply(event, "ping") } diff --git a/internal/messaging/message.go b/internal/messaging/message.go new file mode 100644 index 0000000..08f02f6 --- /dev/null +++ b/internal/messaging/message.go @@ -0,0 +1,51 @@ +package messaging + +import ( + "log/slog" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" +) + +// SendOptions configures message sending behavior +type SendOptions struct { + Reply bool // If true, reply to the triggering message +} + +// SendMessage sends a message with optional reply. +// Errors are logged but not returned to user. +// Returns error for caller's information if needed. +func SendMessage(event *events.MessageCreate, content string, opts *SendOptions) error { + if opts == nil { + opts = &SendOptions{} + } + + builder := discord.NewMessageCreate().WithContent(content) + + if opts.Reply { + messageID := event.Message.ID + builder = builder.WithMessageReference(&discord.MessageReference{ + MessageID: &messageID, + }) + } + + _, err := event.Client().Rest.CreateMessage(event.ChannelID, builder) + if err != nil { + slog.Error("failed to send message", + slog.String("channel_id", event.ChannelID.String()), + slog.String("content", content), + slog.Any("error", err), + ) + } + return err +} + +// Send sends a simple message (no reply) +func Send(event *events.MessageCreate, content string) error { + return SendMessage(event, content, nil) +} + +// SendReply sends a message as a reply to the user's message +func SendReply(event *events.MessageCreate, content string) error { + return SendMessage(event, content, &SendOptions{Reply: true}) +}