7.3 KiB
Creating a Plugin
Plugin Categories
ButterRobot organizes plugins into different categories:
- Development: Utility plugins like
ping
- Fun: Entertainment plugins like dice rolling, coin flipping
- Social: Social media related plugins like URL transformers/expanders
- Security: Moderation and protection features like domain blocking
When creating a new plugin, consider which category it fits into and place it in the appropriate directory.
Plugin Examples
Basic Example: Marco Polo
This simple "Marco Polo" plugin will answer Polo to the user that says Marco:
package myplugin
import (
"strings"
"git.nakama.town/fmartingr/butterrobot/internal/model"
"git.nakama.town/fmartingr/butterrobot/internal/plugin"
)
// MarcoPlugin is a simple Marco/Polo plugin
type MarcoPlugin struct {
plugin.BasePlugin
}
// New creates a new MarcoPlugin instance
func New() *MarcoPlugin {
return &MarcoPlugin{
BasePlugin: plugin.BasePlugin{
ID: "test.marco",
Name: "Marco/Polo",
Help: "Responds to 'Marco' with 'Polo'",
},
}
}
// OnMessage handles incoming messages
func (p *MarcoPlugin) OnMessage(msg *model.Message, config map[string]interface{}) []*model.Message {
if !strings.EqualFold(strings.TrimSpace(msg.Text), "Marco") {
return nil
}
response := &model.Message{
Text: "Polo",
Chat: msg.Chat,
ReplyTo: msg.ID,
Channel: msg.Channel,
}
return []*model.Message{response}
}
Configuration-Enabled Plugin
This plugin requires configuration to be set in the admin interface. It demonstrates how to create plugins that need channel-specific configuration:
package security
import (
"fmt"
"regexp"
"strings"
"git.nakama.town/fmartingr/butterrobot/internal/model"
"git.nakama.town/fmartingr/butterrobot/internal/plugin"
)
// DomainBlockPlugin is a plugin that blocks messages containing links from specific domains
type DomainBlockPlugin struct {
plugin.BasePlugin
}
// New creates a new DomainBlockPlugin instance
func New() *DomainBlockPlugin {
return &DomainBlockPlugin{
BasePlugin: plugin.BasePlugin{
ID: "security.domainblock",
Name: "Domain Blocker",
Help: "Blocks messages containing links from configured domains",
ConfigRequired: true, // Mark this plugin as requiring configuration
},
}
}
// OnMessage processes incoming messages
func (p *DomainBlockPlugin) OnMessage(msg *model.Message, config map[string]interface{}) []*model.Message {
// Get blocked domains from config
blockedDomainsStr, ok := config["blocked_domains"].(string)
if !ok || blockedDomainsStr == "" {
return nil // No blocked domains configured
}
// Split and clean blocked domains
blockedDomains := strings.Split(blockedDomainsStr, ",")
for i, domain := range blockedDomains {
blockedDomains[i] = strings.ToLower(strings.TrimSpace(domain))
}
// Extract domains from message
urlRegex := regexp.MustCompile(`https?://([^\s/$.?#].[^\s]*)`)
matches := urlRegex.FindAllStringSubmatch(msg.Text, -1)
// Check if any extracted domains are blocked
for _, match := range matches {
if len(match) < 2 {
continue
}
domain := strings.ToLower(match[1])
for _, blockedDomain := range blockedDomains {
if blockedDomain == "" {
continue
}
if strings.HasSuffix(domain, blockedDomain) || domain == blockedDomain {
// Domain is blocked, create warning message
response := &model.Message{
Text: fmt.Sprintf("⚠️ Message contained a link to blocked domain: %s", blockedDomain),
Chat: msg.Chat,
ReplyTo: msg.ID,
Channel: msg.Channel,
}
return []*model.Message{response}
}
}
}
return nil
}
func init() {
plugin.Register(New())
}
Advanced Example: URL Transformer
This more complex plugin transforms URLs, useful for improving media embedding in chat platforms:
package social
import (
"net/url"
"regexp"
"strings"
"git.nakama.town/fmartingr/butterrobot/internal/model"
"git.nakama.town/fmartingr/butterrobot/internal/plugin"
)
// TwitterExpander transforms twitter.com links to fxtwitter.com links
type TwitterExpander struct {
plugin.BasePlugin
}
// New creates a new TwitterExpander instance
func NewTwitter() *TwitterExpander {
return &TwitterExpander{
BasePlugin: plugin.BasePlugin{
ID: "social.twitter",
Name: "Twitter Link Expander",
Help: "Automatically converts twitter.com links to fxtwitter.com links and removes tracking parameters",
},
}
}
// OnMessage handles incoming messages
func (p *TwitterExpander) OnMessage(msg *model.Message, config map[string]interface{}) []*model.Message {
// Skip empty messages
if strings.TrimSpace(msg.Text) == "" {
return nil
}
// Regex to match twitter.com links
twitterRegex := regexp.MustCompile(`https?://(www\.)?(twitter\.com|x\.com)/[^\s]+`)
// Check if the message contains a Twitter link
if !twitterRegex.MatchString(msg.Text) {
return nil
}
// Transform the URL
transformed := twitterRegex.ReplaceAllStringFunc(msg.Text, func(link string) string {
// Parse the URL
parsedURL, err := url.Parse(link)
if err != nil {
// If parsing fails, just do the simple replacement
link = strings.Replace(link, "twitter.com", "fxtwitter.com", 1)
link = strings.Replace(link, "x.com", "fxtwitter.com", 1)
return link
}
// Change the host
if strings.Contains(parsedURL.Host, "twitter.com") {
parsedURL.Host = strings.Replace(parsedURL.Host, "twitter.com", "fxtwitter.com", 1)
} else if strings.Contains(parsedURL.Host, "x.com") {
parsedURL.Host = strings.Replace(parsedURL.Host, "x.com", "fxtwitter.com", 1)
}
// Remove query parameters
parsedURL.RawQuery = ""
// Return the cleaned URL
return parsedURL.String()
})
// Create response message
response := &model.Message{
Text: transformed,
Chat: msg.Chat,
ReplyTo: msg.ID,
Channel: msg.Channel,
}
return []*model.Message{response}
}
Enabling Configuration for Plugins
To indicate that your plugin requires configuration:
-
Set
ConfigRequired: true
in the BasePlugin struct:BasePlugin: plugin.BasePlugin{ ID: "myplugin.id", Name: "Plugin Name", Help: "Help text", ConfigRequired: true, },
-
Access the configuration in the OnMessage method:
func (p *MyPlugin) OnMessage(msg *model.Message, config map[string]interface{}) []*model.Message { // Extract configuration values configValue, ok := config["some_config_key"].(string) if !ok || configValue == "" { // Handle missing or empty configuration return nil } // Use the configuration... }
-
The admin interface will show a "Configure" button for plugins that require configuration.
Registering Plugins
To use the plugin, register it in your application:
// In app.go or similar initialization file
func (a *App) Run() error {
// ...
// Register plugins
plugin.Register(ping.New()) // Development plugin
plugin.Register(fun.NewCoin()) // Fun plugin
plugin.Register(social.NewTwitter()) // Social media plugin
plugin.Register(myplugin.New()) // Your custom plugin
// ...
}
Alternatively, you can register your plugin in its init() function:
func init() {
plugin.Register(New())
}