# 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_: ```go 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: ```go 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: ```go 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: 1. Set `ConfigRequired: true` in the BasePlugin struct: ```go BasePlugin: plugin.BasePlugin{ ID: "myplugin.id", Name: "Plugin Name", Help: "Help text", ConfigRequired: true, }, ``` 2. Access the configuration in the OnMessage method: ```go 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... } ``` 3. The admin interface will show a "Configure" button for plugins that require configuration. ## Registering Plugins To use the plugin, register it in your application: ```go // 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: ```go func init() { plugin.Register(New()) } ```