feat: added help command
This commit is contained in:
parent
4fc5ae63a1
commit
bd9854676d
10 changed files with 414 additions and 7 deletions
164
internal/plugin/help/help.go
Normal file
164
internal/plugin/help/help.go
Normal file
|
@ -0,0 +1,164 @@
|
|||
package help
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.nakama.town/fmartingr/butterrobot/internal/db"
|
||||
"git.nakama.town/fmartingr/butterrobot/internal/model"
|
||||
"git.nakama.town/fmartingr/butterrobot/internal/plugin"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
// ChannelPluginGetter is an interface for getting channel plugins
|
||||
type ChannelPluginGetter interface {
|
||||
GetChannelPlugins(channelID int64) ([]*model.ChannelPlugin, error)
|
||||
GetChannelPluginsFromPlatformID(platform, platformChannelID string) ([]*model.ChannelPlugin, error)
|
||||
}
|
||||
|
||||
// HelpPlugin provides help information about available commands
|
||||
type HelpPlugin struct {
|
||||
plugin.BasePlugin
|
||||
db ChannelPluginGetter
|
||||
}
|
||||
|
||||
// New creates a new HelpPlugin instance
|
||||
func New(db ChannelPluginGetter) *HelpPlugin {
|
||||
return &HelpPlugin{
|
||||
BasePlugin: plugin.BasePlugin{
|
||||
ID: "utility.help",
|
||||
Name: "Help",
|
||||
Help: "Shows available commands when you type '!help'",
|
||||
},
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// OnMessage handles incoming messages
|
||||
func (p *HelpPlugin) OnMessage(msg *model.Message, config map[string]interface{}, cache model.CacheInterface) []*model.MessageAction {
|
||||
// Check if message is the help command
|
||||
if !strings.EqualFold(strings.TrimSpace(msg.Text), "!help") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get channel plugins from database using platform and platform channel ID
|
||||
channelPlugins, err := p.db.GetChannelPluginsFromPlatformID(msg.Channel.Platform, msg.Channel.PlatformChannelID)
|
||||
if err != nil && err != db.ErrNotFound {
|
||||
slog.Error("Failed to get channel plugins", slog.Any("err", err))
|
||||
return []*model.MessageAction{}
|
||||
}
|
||||
|
||||
// If no plugins found, initialize empty slice
|
||||
if err == db.ErrNotFound {
|
||||
channelPlugins = []*model.ChannelPlugin{}
|
||||
}
|
||||
|
||||
// Get all available plugins
|
||||
availablePlugins := plugin.GetAvailablePlugins()
|
||||
|
||||
// Filter to only enabled plugins for this channel
|
||||
enabledPlugins := make(map[string]model.Plugin)
|
||||
for _, channelPlugin := range channelPlugins {
|
||||
if channelPlugin.Enabled {
|
||||
if availablePlugin, exists := availablePlugins[channelPlugin.PluginID]; exists {
|
||||
enabledPlugins[channelPlugin.PluginID] = availablePlugin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no plugins are enabled, return a message
|
||||
if len(enabledPlugins) == 0 {
|
||||
response := &model.Message{
|
||||
Text: "No plugins are currently enabled for this channel.",
|
||||
Chat: msg.Chat,
|
||||
ReplyTo: msg.ID,
|
||||
Channel: msg.Channel,
|
||||
}
|
||||
|
||||
return []*model.MessageAction{
|
||||
{
|
||||
Type: model.ActionSendMessage,
|
||||
Message: response,
|
||||
Chat: msg.Chat,
|
||||
Channel: msg.Channel,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Group plugins by category
|
||||
categories := map[string][]model.Plugin{
|
||||
"Development": {},
|
||||
"Fun and Entertainment": {},
|
||||
"Utility": {},
|
||||
"Security": {},
|
||||
"Social Media": {},
|
||||
"Other": {},
|
||||
}
|
||||
|
||||
// Categorize plugins based on their ID prefix
|
||||
for _, p := range enabledPlugins {
|
||||
category := p.GetID()
|
||||
switch {
|
||||
case strings.HasPrefix(category, "dev."):
|
||||
categories["Development"] = append(categories["Development"], p)
|
||||
case strings.HasPrefix(category, "fun."):
|
||||
categories["Fun and Entertainment"] = append(categories["Fun and Entertainment"], p)
|
||||
case strings.HasPrefix(category, "util.") || strings.HasPrefix(category, "reminder.") || strings.HasPrefix(category, "utility."):
|
||||
categories["Utility"] = append(categories["Utility"], p)
|
||||
case strings.HasPrefix(category, "security."):
|
||||
categories["Security"] = append(categories["Security"], p)
|
||||
case strings.HasPrefix(category, "social."):
|
||||
categories["Social Media"] = append(categories["Social Media"], p)
|
||||
default:
|
||||
categories["Other"] = append(categories["Other"], p)
|
||||
}
|
||||
}
|
||||
|
||||
// Build the help message
|
||||
var helpText strings.Builder
|
||||
helpText.WriteString("🤖 **Available Commands**\n\n")
|
||||
|
||||
// Sort category names for consistent output
|
||||
categoryOrder := []string{"Development", "Fun and Entertainment", "Utility", "Security", "Social Media", "Other"}
|
||||
|
||||
for _, categoryName := range categoryOrder {
|
||||
pluginList := categories[categoryName]
|
||||
if len(pluginList) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Sort plugins within category by name
|
||||
sort.Slice(pluginList, func(i, j int) bool {
|
||||
return pluginList[i].GetName() < pluginList[j].GetName()
|
||||
})
|
||||
|
||||
helpText.WriteString(fmt.Sprintf("**%s:**\n", categoryName))
|
||||
for _, p := range pluginList {
|
||||
if p.GetHelp() == "" {
|
||||
continue
|
||||
}
|
||||
helpText.WriteString(fmt.Sprintf("• **%s** - %s\n", p.GetName(), p.GetHelp()))
|
||||
}
|
||||
helpText.WriteString("\n")
|
||||
}
|
||||
|
||||
// Add footer
|
||||
helpText.WriteString("_Use the specific commands or triggers mentioned above to interact with the bot._")
|
||||
|
||||
response := &model.Message{
|
||||
Text: helpText.String(),
|
||||
Chat: msg.Chat,
|
||||
ReplyTo: msg.ID,
|
||||
Channel: msg.Channel,
|
||||
}
|
||||
|
||||
return []*model.MessageAction{
|
||||
{
|
||||
Type: model.ActionSendMessage,
|
||||
Message: response,
|
||||
Chat: msg.Chat,
|
||||
Channel: msg.Channel,
|
||||
},
|
||||
}
|
||||
}
|
206
internal/plugin/help/help_test.go
Normal file
206
internal/plugin/help/help_test.go
Normal file
|
@ -0,0 +1,206 @@
|
|||
package help
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.nakama.town/fmartingr/butterrobot/internal/db"
|
||||
"git.nakama.town/fmartingr/butterrobot/internal/model"
|
||||
"git.nakama.town/fmartingr/butterrobot/internal/plugin"
|
||||
)
|
||||
|
||||
// MockPlugin implements the Plugin interface for testing
|
||||
type MockPlugin struct {
|
||||
id string
|
||||
name string
|
||||
help string
|
||||
}
|
||||
|
||||
func (m *MockPlugin) GetID() string { return m.id }
|
||||
func (m *MockPlugin) GetName() string { return m.name }
|
||||
func (m *MockPlugin) GetHelp() string { return m.help }
|
||||
func (m *MockPlugin) RequiresConfig() bool {
|
||||
return false
|
||||
}
|
||||
func (m *MockPlugin) OnMessage(msg *model.Message, config map[string]interface{}, cache model.CacheInterface) []*model.MessageAction {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MockDatabase implements the ChannelPluginGetter interface for testing
|
||||
type MockDatabase struct {
|
||||
channelPlugins map[int64][]*model.ChannelPlugin
|
||||
platformChannelPlugins map[string][]*model.ChannelPlugin // key: "platform:platformChannelID"
|
||||
}
|
||||
|
||||
func (m *MockDatabase) GetChannelPlugins(channelID int64) ([]*model.ChannelPlugin, error) {
|
||||
if plugins, exists := m.channelPlugins[channelID]; exists {
|
||||
return plugins, nil
|
||||
}
|
||||
return nil, db.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *MockDatabase) GetChannelPluginsFromPlatformID(platform, platformChannelID string) ([]*model.ChannelPlugin, error) {
|
||||
key := platform + ":" + platformChannelID
|
||||
if plugins, exists := m.platformChannelPlugins[key]; exists {
|
||||
return plugins, nil
|
||||
}
|
||||
return nil, db.ErrNotFound
|
||||
}
|
||||
|
||||
func TestHelpPlugin_OnMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
messageText string
|
||||
enabledPlugins map[string]*MockPlugin
|
||||
expectResponse bool
|
||||
expectNoPlugins bool
|
||||
expectCategories []string
|
||||
}{
|
||||
{
|
||||
name: "responds to !help command",
|
||||
messageText: "!help",
|
||||
enabledPlugins: map[string]*MockPlugin{
|
||||
"dev.ping": {
|
||||
id: "dev.ping",
|
||||
name: "Ping",
|
||||
help: "Responds to 'ping' with 'pong'",
|
||||
},
|
||||
"fun.dice": {
|
||||
id: "fun.dice",
|
||||
name: "Dice Roller",
|
||||
help: "Rolls dice when you type '!dice [formula]'",
|
||||
},
|
||||
},
|
||||
expectResponse: true,
|
||||
expectCategories: []string{"Development", "Fun and Entertainment"},
|
||||
},
|
||||
{
|
||||
name: "ignores non-help messages",
|
||||
messageText: "hello world",
|
||||
enabledPlugins: map[string]*MockPlugin{},
|
||||
expectResponse: false,
|
||||
},
|
||||
{
|
||||
name: "ignores case variation",
|
||||
messageText: "!HELP",
|
||||
enabledPlugins: map[string]*MockPlugin{},
|
||||
expectResponse: true,
|
||||
expectNoPlugins: true,
|
||||
},
|
||||
{
|
||||
name: "handles no enabled plugins",
|
||||
messageText: "!help",
|
||||
enabledPlugins: map[string]*MockPlugin{},
|
||||
expectResponse: true,
|
||||
expectNoPlugins: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create mock database
|
||||
mockDB := &MockDatabase{
|
||||
channelPlugins: make(map[int64][]*model.ChannelPlugin),
|
||||
platformChannelPlugins: make(map[string][]*model.ChannelPlugin),
|
||||
}
|
||||
|
||||
// Setup channel plugins in mock database
|
||||
var channelPluginList []*model.ChannelPlugin
|
||||
pluginCounter := int64(1)
|
||||
for pluginID := range tt.enabledPlugins {
|
||||
channelPluginList = append(channelPluginList, &model.ChannelPlugin{
|
||||
ID: pluginCounter,
|
||||
ChannelID: 1,
|
||||
PluginID: pluginID,
|
||||
Enabled: true,
|
||||
Config: make(map[string]interface{}),
|
||||
})
|
||||
pluginCounter++
|
||||
}
|
||||
|
||||
// Set up both mapping approaches for the test
|
||||
mockDB.channelPlugins[1] = channelPluginList
|
||||
mockDB.platformChannelPlugins["test:test-channel"] = channelPluginList
|
||||
|
||||
// Create help plugin
|
||||
p := New(mockDB)
|
||||
|
||||
// Create mock channel
|
||||
channel := &model.Channel{
|
||||
ID: 1,
|
||||
Platform: "test",
|
||||
PlatformChannelID: "test-channel",
|
||||
}
|
||||
|
||||
// Create test message
|
||||
msg := &model.Message{
|
||||
ID: "test-msg",
|
||||
Text: tt.messageText,
|
||||
Chat: "test-chat",
|
||||
Channel: channel,
|
||||
}
|
||||
|
||||
// Mock the plugin registry
|
||||
originalRegistry := plugin.GetAvailablePlugins()
|
||||
|
||||
// Override the registry for this test
|
||||
plugin.ClearRegistry()
|
||||
for _, mockPlugin := range tt.enabledPlugins {
|
||||
plugin.Register(mockPlugin)
|
||||
}
|
||||
|
||||
// Call OnMessage
|
||||
actions := p.OnMessage(msg, map[string]interface{}{}, nil)
|
||||
|
||||
// Restore original registry
|
||||
plugin.ClearRegistry()
|
||||
for _, p := range originalRegistry {
|
||||
plugin.Register(p)
|
||||
}
|
||||
|
||||
if !tt.expectResponse {
|
||||
if len(actions) != 0 {
|
||||
t.Errorf("Expected no response, but got %d actions", len(actions))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(actions) != 1 {
|
||||
t.Errorf("Expected 1 action, got %d", len(actions))
|
||||
return
|
||||
}
|
||||
|
||||
action := actions[0]
|
||||
if action.Type != model.ActionSendMessage {
|
||||
t.Errorf("Expected ActionSendMessage, got %v", action.Type)
|
||||
return
|
||||
}
|
||||
|
||||
responseText := action.Message.Text
|
||||
|
||||
if tt.expectNoPlugins {
|
||||
if !strings.Contains(responseText, "No plugins are currently enabled") {
|
||||
t.Errorf("Expected 'no plugins' message, got: %s", responseText)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check that expected categories appear in response
|
||||
for _, category := range tt.expectCategories {
|
||||
if !strings.Contains(responseText, "**"+category+":**") {
|
||||
t.Errorf("Expected category '%s' in response, got: %s", category, responseText)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that plugin names and help text appear
|
||||
for _, mockPlugin := range tt.enabledPlugins {
|
||||
if !strings.Contains(responseText, mockPlugin.GetName()) {
|
||||
t.Errorf("Expected plugin name '%s' in response", mockPlugin.GetName())
|
||||
}
|
||||
if !strings.Contains(responseText, mockPlugin.GetHelp()) {
|
||||
t.Errorf("Expected plugin help '%s' in response", mockPlugin.GetHelp())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue