Compare commits
No commits in common. "3a4ba376e75503caab4da111c4941969f9793c0e" and "4fc5ae63a1bad41188ebed9e4b37f106f9c10c4a" have entirely different histories.
3a4ba376e7
...
4fc5ae63a1
11 changed files with 8 additions and 415 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -11,6 +11,6 @@ dist
|
||||||
bin
|
bin
|
||||||
# Butterrobot
|
# Butterrobot
|
||||||
*.sqlite*
|
*.sqlite*
|
||||||
butterrobot.db*
|
butterrobot.db
|
||||||
/butterrobot
|
/butterrobot
|
||||||
*_test.db*
|
*_test.db*
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
|
|
||||||
### Utility
|
### Utility
|
||||||
|
|
||||||
- Help: Shows available commands when you type `!help`. Lists all enabled plugins for the current channel organized by category with their descriptions and usage instructions.
|
|
||||||
- Remind Me: Reply to a message with `!remindme <duration>` to set a reminder. Supported duration units: y (years), mo (months), d (days), h (hours), m (minutes), s (seconds). Examples: `!remindme 1y` for 1 year, `!remindme 3mo` for 3 months, `!remindme 2d` for 2 days, `!remindme 3h` for 3 hours. The bot will mention you with a reminder after the specified time.
|
- Remind Me: Reply to a message with `!remindme <duration>` to set a reminder. Supported duration units: y (years), mo (months), d (days), h (hours), m (minutes), s (seconds). Examples: `!remindme 1y` for 1 year, `!remindme 3mo` for 3 months, `!remindme 2d` for 2 days, `!remindme 3h` for 3 hours. The bot will mention you with a reminder after the specified time.
|
||||||
- Search and Replace: Reply to any message with `s/search/replace/[flags]` to perform text substitution. Supports flags: `g` (global), `i` (case insensitive), `n` (regex pattern). Example: `s/hello/hi/gi` replaces all occurrences of "hello" with "hi" case-insensitively.
|
- Search and Replace: Reply to any message with `s/search/replace/[flags]` to perform text substitution. Supports flags: `g` (global), `i` (case insensitive), `n` (regex pattern). Example: `s/hello/hi/gi` replaces all occurrences of "hello" with "hi" case-insensitively.
|
||||||
|
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -6,7 +6,6 @@ require (
|
||||||
github.com/gorilla/sessions v1.4.0
|
github.com/gorilla/sessions v1.4.0
|
||||||
golang.org/x/crypto v0.37.0
|
golang.org/x/crypto v0.37.0
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250418111936-9c1aa6af88df
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20250418111936-9c1aa6af88df
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
|
|
||||||
modernc.org/sqlite v1.37.0
|
modernc.org/sqlite v1.37.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,6 +16,7 @@ require (
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||||
golang.org/x/sys v0.32.0 // indirect
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
modernc.org/libc v1.63.0 // indirect
|
modernc.org/libc v1.63.0 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
|
|
@ -23,7 +23,6 @@ import (
|
||||||
"git.nakama.town/fmartingr/butterrobot/internal/plugin"
|
"git.nakama.town/fmartingr/butterrobot/internal/plugin"
|
||||||
"git.nakama.town/fmartingr/butterrobot/internal/plugin/domainblock"
|
"git.nakama.town/fmartingr/butterrobot/internal/plugin/domainblock"
|
||||||
"git.nakama.town/fmartingr/butterrobot/internal/plugin/fun"
|
"git.nakama.town/fmartingr/butterrobot/internal/plugin/fun"
|
||||||
"git.nakama.town/fmartingr/butterrobot/internal/plugin/help"
|
|
||||||
"git.nakama.town/fmartingr/butterrobot/internal/plugin/ping"
|
"git.nakama.town/fmartingr/butterrobot/internal/plugin/ping"
|
||||||
"git.nakama.town/fmartingr/butterrobot/internal/plugin/reminder"
|
"git.nakama.town/fmartingr/butterrobot/internal/plugin/reminder"
|
||||||
"git.nakama.town/fmartingr/butterrobot/internal/plugin/searchreplace"
|
"git.nakama.town/fmartingr/butterrobot/internal/plugin/searchreplace"
|
||||||
|
@ -95,7 +94,6 @@ func (a *App) Run() error {
|
||||||
plugin.Register(reminder.New(a.db))
|
plugin.Register(reminder.New(a.db))
|
||||||
plugin.Register(domainblock.New())
|
plugin.Register(domainblock.New())
|
||||||
plugin.Register(searchreplace.New())
|
plugin.Register(searchreplace.New())
|
||||||
plugin.Register(help.New(a.db))
|
|
||||||
|
|
||||||
// Initialize routes
|
// Initialize routes
|
||||||
a.initializeRoutes()
|
a.initializeRoutes()
|
||||||
|
|
|
@ -261,7 +261,7 @@ func (d *Database) GetChannelPlugins(channelID int64) ([]*model.ChannelPlugin, e
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse config JSON
|
// Parse config JSON
|
||||||
var config map[string]any
|
var config map[string]interface{}
|
||||||
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
|
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -288,28 +288,6 @@ func (d *Database) GetChannelPlugins(channelID int64) ([]*model.ChannelPlugin, e
|
||||||
return plugins, nil
|
return plugins, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetChannelPluginsFromPlatformID retrieves all plugins for a channel by platform and platform channel ID
|
|
||||||
func (d *Database) GetChannelPluginsFromPlatformID(platform, platformChannelID string) ([]*model.ChannelPlugin, error) {
|
|
||||||
// First, get the channel ID by platform and platform channel ID
|
|
||||||
query := `
|
|
||||||
SELECT id
|
|
||||||
FROM channels
|
|
||||||
WHERE platform = ? AND platform_channel_id = ?
|
|
||||||
`
|
|
||||||
|
|
||||||
var channelID int64
|
|
||||||
err := d.db.QueryRow(query, platform, platformChannelID).Scan(&channelID)
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, ErrNotFound
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now get the plugins for this channel
|
|
||||||
return d.GetChannelPlugins(channelID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetChannelPluginByID retrieves a channel plugin by ID
|
// GetChannelPluginByID retrieves a channel plugin by ID
|
||||||
func (d *Database) GetChannelPluginByID(id int64) (*model.ChannelPlugin, error) {
|
func (d *Database) GetChannelPluginByID(id int64) (*model.ChannelPlugin, error) {
|
||||||
query := `
|
query := `
|
||||||
|
@ -648,8 +626,8 @@ func (d *Database) UpdateUserPassword(userID int64, newPassword string) error {
|
||||||
func (d *Database) CreateReminder(platform, channelID, messageID, replyToID, userID, username, content string, triggerAt time.Time) (*model.Reminder, error) {
|
func (d *Database) CreateReminder(platform, channelID, messageID, replyToID, userID, username, content string, triggerAt time.Time) (*model.Reminder, error) {
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO reminders (
|
INSERT INTO reminders (
|
||||||
platform, channel_id, message_id, reply_to_id,
|
platform, channel_id, message_id, reply_to_id,
|
||||||
user_id, username, created_at, trigger_at,
|
user_id, username, created_at, trigger_at,
|
||||||
content, processed
|
content, processed
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
||||||
`
|
`
|
||||||
|
@ -688,7 +666,7 @@ func (d *Database) CreateReminder(platform, channelID, messageID, replyToID, use
|
||||||
// GetPendingReminders gets all pending reminders that need to be processed
|
// GetPendingReminders gets all pending reminders that need to be processed
|
||||||
func (d *Database) GetPendingReminders() ([]*model.Reminder, error) {
|
func (d *Database) GetPendingReminders() ([]*model.Reminder, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT id, platform, channel_id, message_id, reply_to_id,
|
SELECT id, platform, channel_id, message_id, reply_to_id,
|
||||||
user_id, username, created_at, trigger_at, content, processed
|
user_id, username, created_at, trigger_at, content, processed
|
||||||
FROM reminders
|
FROM reminders
|
||||||
WHERE processed = 0 AND trigger_at <= ?
|
WHERE processed = 0 AND trigger_at <= ?
|
||||||
|
|
|
@ -70,7 +70,7 @@ func NewHLTB() *HLTBPlugin {
|
||||||
BasePlugin: plugin.BasePlugin{
|
BasePlugin: plugin.BasePlugin{
|
||||||
ID: "fun.hltb",
|
ID: "fun.hltb",
|
||||||
Name: "How Long To Beat",
|
Name: "How Long To Beat",
|
||||||
Help: "Get game completion times from HowLongToBeat.com using `!hltb <game name>`",
|
Help: "Get game completion times from HowLongToBeat.com using !hltb <game name>",
|
||||||
},
|
},
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
|
|
|
@ -23,11 +23,6 @@ func NewLoquito() *LoquitoPlugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHelp returns the plugin help text
|
|
||||||
func (p *LoquitoPlugin) GetHelp() string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnMessage handles incoming messages
|
// OnMessage handles incoming messages
|
||||||
func (p *LoquitoPlugin) OnMessage(msg *model.Message, config map[string]interface{}, cache model.CacheInterface) []*model.MessageAction {
|
func (p *LoquitoPlugin) OnMessage(msg *model.Message, config map[string]interface{}, cache model.CacheInterface) []*model.MessageAction {
|
||||||
if !strings.Contains(strings.ToLower(msg.Text), "lo quito") {
|
if !strings.Contains(strings.ToLower(msg.Text), "lo quito") {
|
||||||
|
|
|
@ -1,164 +0,0 @@
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,206 +0,0 @@
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -47,13 +47,6 @@ func GetAvailablePlugins() map[string]model.Plugin {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearRegistry clears all registered plugins (for testing)
|
|
||||||
func ClearRegistry() {
|
|
||||||
pluginsMu.Lock()
|
|
||||||
defer pluginsMu.Unlock()
|
|
||||||
plugins = make(map[string]model.Plugin)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BasePlugin provides a common base for plugins
|
// BasePlugin provides a common base for plugins
|
||||||
type BasePlugin struct {
|
type BasePlugin struct {
|
||||||
ID string
|
ID string
|
||||||
|
|
|
@ -23,7 +23,7 @@ func New() *SearchReplacePlugin {
|
||||||
BasePlugin: plugin.BasePlugin{
|
BasePlugin: plugin.BasePlugin{
|
||||||
ID: "util.searchreplace",
|
ID: "util.searchreplace",
|
||||||
Name: "Search and Replace",
|
Name: "Search and Replace",
|
||||||
Help: "Reply to a message with a search and replace pattern (`s/search/replace/[flags]`) to create a modified message. " +
|
Help: "Reply to a message with a search and replace pattern (s/search/replace/[flags]) to create a modified message. " +
|
||||||
"Supported flags: g (global), i (case insensitive)",
|
"Supported flags: g (global), i (case insensitive)",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue